mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
52 Commits
blur
...
d7fb75f7f9
| Author | SHA1 | Date | |
|---|---|---|---|
| d7fb75f7f9 | |||
| cf0fa7da6b | |||
| 787d213722 | |||
| 2138fbf8b7 | |||
| 722b3fd1e8 | |||
| 2728296cbd | |||
| fe1fd92953 | |||
| 0ab9b1e4e9 | |||
| 6d0953de68 | |||
| bc6bbdbe9d | |||
| eff728fdf5 | |||
| 8d415e9568 | |||
| e6ed6a1cc2 | |||
| ca18174da5 | |||
| 976b231b93 | |||
| 3d75a51378 | |||
| dc4b1529e6 | |||
| f61438e11f | |||
| 8f78163941 | |||
| f894d338fc | |||
| f2df53afcd | |||
| 4179fcee83 | |||
| a0c9af1ee7 | |||
| 049266271a | |||
| 0eabda3164 | |||
| 32c063aab8 | |||
| 37f92677cf | |||
| 13e8130858 | |||
| f6e590a518 | |||
| 3194fc3fbe | |||
| 3318864ece | |||
| e224417593 | |||
| 3f7f6c5d2c | |||
| 0b88055742 | |||
| 2b0826e397 | |||
| 7db04c9660 | |||
| 14d1e1d985 | |||
| 903ab1e61d | |||
| 5982655539 | |||
| 1021a210cf | |||
| e34edb15bb | |||
| 61ee5f4336 | |||
| ce2a92ec27 | |||
| 66ce79b9bf | |||
| 30dd640314 | |||
| 28f9aabcd9 | |||
| 3d9bd73336 | |||
| 3497d5f523 | |||
| 8ef1d95e65 | |||
| e9aeb9ac60 | |||
| fb02f7294d | |||
| f15d49d80a |
@@ -13,7 +13,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
|||||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
[](https://archlinux.org/packages/extra/x86_64/dms-shell/)
|
||||||
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/danklinux)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
|
|||||||
+41
-1
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
|
|||||||
Command-line interface and daemon for shell management and system control.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**dankinstall**
|
||||||
Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
|
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
@@ -147,10 +147,50 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
|||||||
|
|
||||||
## Installation via dankinstall
|
## Installation via dankinstall
|
||||||
|
|
||||||
|
**Interactive (TUI):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://install.danklinux.com | sh
|
curl -fsSL https://install.danklinux.com | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Headless (unattended):**
|
||||||
|
|
||||||
|
Headless mode requires cached sudo credentials. Run `sudo -v` first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
|
||||||
|
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Short | Description |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
|
||||||
|
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
|
||||||
|
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
|
||||||
|
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
||||||
|
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
||||||
|
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
||||||
|
| `--yes` | `-y` | Required for headless mode — confirms installation without interactive prompts |
|
||||||
|
|
||||||
|
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
||||||
|
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
||||||
|
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
|
||||||
|
|
||||||
|
When no flags are provided, `dankinstall` launches the interactive TUI.
|
||||||
|
|
||||||
|
### Headless mode validation rules
|
||||||
|
|
||||||
|
Headless mode activates when `--compositor` or `--term` is provided.
|
||||||
|
|
||||||
|
- Both `--compositor` and `--term` are required; providing only one results in an error.
|
||||||
|
- Headless-only flags (`--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`) are rejected in TUI mode.
|
||||||
|
- Positional arguments are not accepted.
|
||||||
|
|
||||||
|
### Log file location
|
||||||
|
|
||||||
|
`dankinstall` writes logs to `/tmp` by default.
|
||||||
|
Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory.
|
||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
||||||
|
|||||||
@@ -3,20 +3,152 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/headless"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
|
// Flag variables bound via pflag
|
||||||
|
var (
|
||||||
|
compositor string
|
||||||
|
term string
|
||||||
|
includeDeps []string
|
||||||
|
excludeDeps []string
|
||||||
|
replaceConfigs []string
|
||||||
|
replaceConfigsAll bool
|
||||||
|
yes bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "dankinstall",
|
||||||
|
Short: "Install DankMaterialShell and its dependencies",
|
||||||
|
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal.
|
||||||
|
|
||||||
|
Without flags, it launches an interactive TUI. Providing either --compositor
|
||||||
|
or --term activates headless (unattended) mode, which requires both flags.
|
||||||
|
|
||||||
|
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
|
||||||
|
configure passwordless sudo for your user.`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: runDankinstall,
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (enables headless mode)")
|
||||||
|
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
||||||
|
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
||||||
|
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
||||||
|
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
||||||
|
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
||||||
|
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDankinstall(cmd *cobra.Command, args []string) error {
|
||||||
|
headlessMode := compositor != "" || term != ""
|
||||||
|
|
||||||
|
if !headlessMode {
|
||||||
|
// Reject headless-only flags when running in TUI mode.
|
||||||
|
headlessOnly := []string{
|
||||||
|
"include-deps",
|
||||||
|
"exclude-deps",
|
||||||
|
"replace-configs",
|
||||||
|
"replace-configs-all",
|
||||||
|
"yes",
|
||||||
|
}
|
||||||
|
var set []string
|
||||||
|
for _, name := range headlessOnly {
|
||||||
|
if cmd.Flags().Changed(name) {
|
||||||
|
set = append(set, "--"+name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(set) > 0 {
|
||||||
|
return fmt.Errorf("flags %s are only valid in headless mode (requires both --compositor and --term)", strings.Join(set, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if headlessMode {
|
||||||
|
return runHeadless()
|
||||||
|
}
|
||||||
|
return runTUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runHeadless() error {
|
||||||
|
// Validate required flags
|
||||||
|
if compositor == "" {
|
||||||
|
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
|
||||||
|
}
|
||||||
|
if term == "" {
|
||||||
|
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := headless.Config{
|
||||||
|
Compositor: compositor,
|
||||||
|
Terminal: term,
|
||||||
|
IncludeDeps: includeDeps,
|
||||||
|
ExcludeDeps: excludeDeps,
|
||||||
|
ReplaceConfigs: replaceConfigs,
|
||||||
|
ReplaceConfigsAll: replaceConfigsAll,
|
||||||
|
Yes: yes,
|
||||||
|
}
|
||||||
|
|
||||||
|
runner := headless.NewRunner(cfg)
|
||||||
|
|
||||||
|
// Set up file logging
|
||||||
|
fileLogger, err := log.NewFileLogger()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileLogger != nil {
|
||||||
|
fmt.Printf("Logging to: %s\n", fileLogger.GetLogPath())
|
||||||
|
fileLogger.StartListening(runner.GetLogChan())
|
||||||
|
defer func() {
|
||||||
|
if err := fileLogger.Close(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Failed to close log file: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
// Drain the log channel to prevent blocking sends from deadlocking
|
||||||
|
// downstream components (distros, config deployer) that write to it.
|
||||||
|
// Use an explicit stop signal because this code does not own the
|
||||||
|
// runner log channel and cannot assume it will be closed.
|
||||||
|
defer drainLogChan(runner.GetLogChan())()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runner.Run(); err != nil {
|
||||||
|
if fileLogger != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileLogger != nil {
|
||||||
|
fmt.Printf("\nFull logs are available at: %s\n", fileLogger.GetLogPath())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTUI() error {
|
||||||
fileLogger, err := log.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||||
@@ -38,18 +170,50 @@ func main() {
|
|||||||
|
|
||||||
if fileLogger != nil {
|
if fileLogger != nil {
|
||||||
fileLogger.StartListening(model.GetLogChan())
|
fileLogger.StartListening(model.GetLogChan())
|
||||||
|
} else {
|
||||||
|
// Drain the log channel to prevent blocking sends from deadlocking
|
||||||
|
// downstream components (distros, config deployer) that write to it.
|
||||||
|
// Use an explicit stop signal because this code does not own the
|
||||||
|
// model log channel and cannot assume it will be closed.
|
||||||
|
defer drainLogChan(model.GetLogChan())()
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
fmt.Printf("Error running program: %v\n", err)
|
|
||||||
if logFilePath != "" {
|
if logFilePath != "" {
|
||||||
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", logFilePath)
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
return fmt.Errorf("error running program: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if logFilePath != "" {
|
if logFilePath != "" {
|
||||||
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drainLogChan starts a goroutine that discards all messages from logCh,
|
||||||
|
// preventing blocking sends from deadlocking downstream components. It returns
|
||||||
|
// a cleanup function that signals the goroutine to stop and waits for it to
|
||||||
|
// exit. Callers should defer the returned function.
|
||||||
|
func drainLogChan(logCh <-chan string) func() {
|
||||||
|
drainStop := make(chan struct{})
|
||||||
|
drainDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(drainDone)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-drainStop:
|
||||||
|
return
|
||||||
|
case _, ok := <-logCh:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return func() {
|
||||||
|
close(drainStop)
|
||||||
|
<-drainDone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var blurCmd = &cobra.Command{
|
||||||
|
Use: "blur",
|
||||||
|
Short: "Background blur utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var blurCheckCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runBlurCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
blurCmd.AddCommand(blurCheckCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch supported {
|
||||||
|
case true:
|
||||||
|
fmt.Println("supported")
|
||||||
|
default:
|
||||||
|
fmt.Println("unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,6 +236,7 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
defer ddc.Close()
|
defer ddc.Close()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
|
ddc.WaitPending()
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
@@ -820,10 +820,14 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
return !utils.CommandExists(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(terminals) > 0 {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
|||||||
@@ -109,16 +109,41 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
if isArchPackageInstalled("dms-shell-bin") {
|
var isAUR bool
|
||||||
packageName = "dms-shell-bin"
|
if isArchPackageInstalled("dms-shell") {
|
||||||
|
packageName = "dms-shell"
|
||||||
} else if isArchPackageInstalled("dms-shell-git") {
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
packageName = "dms-shell-git"
|
packageName = "dms-shell-git"
|
||||||
|
isAUR = true
|
||||||
|
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||||
|
packageName = "dms-shell-bin"
|
||||||
|
isAUR = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
fmt.Println("Info: No dms-shell package found.")
|
||||||
fmt.Println("Info: Falling back to git-based update method...")
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
return updateOtherDistros()
|
return updateOtherDistros()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isAUR {
|
||||||
|
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||||
|
if !confirmUpdate() {
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
||||||
|
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dms successfully updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +28,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||||
|
// safe to run as root (e.g. shell completion, help).
|
||||||
|
func isReadOnlyCommand(args []string) bool {
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch arg {
|
||||||
|
case "completion", "help", "__complete":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package blur
|
||||||
|
|
||||||
|
import (
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||||
|
|
||||||
|
func ProbeSupport() (bool, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer display.Context().Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case extBackgroundEffectInterface:
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,66 +12,142 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
|
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||||
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
|
func MaybeServeAndExit() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := os.Getenv(envMime)
|
||||||
|
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||||
|
cachePath := os.Getenv(envCacheFile)
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cachePath != "":
|
||||||
|
data, err = os.ReadFile(cachePath)
|
||||||
|
os.Remove(cachePath)
|
||||||
|
default:
|
||||||
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
return serveClipboard(data, mimeType, pasteOnce)
|
||||||
total := 0
|
|
||||||
for total < len(data) {
|
|
||||||
n, err := writer.Write(data[total:])
|
|
||||||
total += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if total != len(data) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if !foreground {
|
if foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
cmd := exec.Command(os.Args[0])
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
cmd.Env = append(os.Environ(),
|
||||||
|
envServe+"=1",
|
||||||
|
envMime+"="+mimeType,
|
||||||
|
)
|
||||||
|
if pasteOnce {
|
||||||
|
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||||
|
}
|
||||||
|
cmd.Env = append(cmd.Env, extra...)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitReady(cmd *exec.Cmd) error {
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdout pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
|
cacheFile, err := createClipboardCacheFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cache file: %w", err)
|
||||||
|
}
|
||||||
|
cachePath := cacheFile.Name()
|
||||||
|
|
||||||
|
if _, err := cacheFile.Write(data); err != nil {
|
||||||
|
cacheFile.Close()
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("write cache file: %w", err)
|
||||||
|
}
|
||||||
|
if err := cacheFile.Close(); err != nil {
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("close cache file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
if err := waitReady(cmd); err != nil {
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
|
|
||||||
switch src := data.(type) {
|
switch src := data.(type) {
|
||||||
case *os.File:
|
case *os.File:
|
||||||
cmd.Stdin = src
|
cmd.Stdin = src
|
||||||
if err := cmd.Start(); err != nil {
|
return waitReady(cmd)
|
||||||
return fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
@@ -83,50 +158,22 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
if err := stdin.Close(); err != nil {
|
if err := stdin.Close(); err != nil {
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
if _, err := stdout.Read(buf[:]); err != nil {
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalReady() {
|
func signalReady() {
|
||||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
if os.Getenv(envServe) == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.Stdout.Write([]byte{1})
|
os.Stdout.Write([]byte{1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
|
||||||
cachedData, err := createClipboardCacheFile()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(cachedData.Name())
|
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
if err := cachedData.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
|
||||||
}
|
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
preferredDirs := []string{}
|
preferredDirs := []string{}
|
||||||
|
|
||||||
@@ -147,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -189,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -233,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
sendErr := make(chan error, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
_, _ = file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -266,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
case err := <-sendErr:
|
|
||||||
return err
|
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
@@ -521,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -554,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
file.Write(data)
|
_, _ = file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -39,11 +39,10 @@ type LayerSurface struct {
|
|||||||
wlSurface *client.Surface
|
wlSurface *client.Surface
|
||||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
viewport *wp_viewporter.WpViewport
|
viewport *wp_viewporter.WpViewport
|
||||||
wlPool *client.ShmPool
|
wlPools [2]*client.ShmPool
|
||||||
wlBuffer *client.Buffer
|
wlBuffers [2]*client.Buffer
|
||||||
bufferBusy bool
|
slotBusy [2]bool
|
||||||
oldPool *client.ShmPool
|
needsRedraw bool
|
||||||
oldBuffer *client.Buffer
|
|
||||||
scopyBuffer *client.Buffer
|
scopyBuffer *client.Buffer
|
||||||
configured bool
|
configured bool
|
||||||
hidden bool
|
hidden bool
|
||||||
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.flushRedraws()
|
||||||
p.checkDone()
|
p.checkDone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +164,15 @@ func (p *Picker) checkDone() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Picker) flushRedraws() {
|
||||||
|
for _, ls := range p.surfaces {
|
||||||
|
if !ls.needsRedraw {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.redrawSurface(ls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Picker) connect() error {
|
func (p *Picker) connect() error {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -507,47 +516,45 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
|
slot := ls.state.FrontIndex()
|
||||||
|
if ls.slotBusy[slot] {
|
||||||
|
ls.needsRedraw = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var renderBuf *ShmBuffer
|
var renderBuf *ShmBuffer
|
||||||
if ls.hidden {
|
switch {
|
||||||
|
case ls.hidden:
|
||||||
renderBuf = ls.state.RedrawScreenOnly()
|
renderBuf = ls.state.RedrawScreenOnly()
|
||||||
} else {
|
default:
|
||||||
renderBuf = ls.state.Redraw()
|
renderBuf = ls.state.Redraw()
|
||||||
}
|
}
|
||||||
if renderBuf == nil {
|
if renderBuf == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ls.oldBuffer != nil {
|
ls.needsRedraw = false
|
||||||
ls.oldBuffer.Destroy()
|
|
||||||
ls.oldBuffer = nil
|
if ls.wlPools[slot] == nil {
|
||||||
}
|
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||||
if ls.oldPool != nil {
|
if err != nil {
|
||||||
ls.oldPool.Destroy()
|
return
|
||||||
ls.oldPool = nil
|
}
|
||||||
|
ls.wlPools[slot] = pool
|
||||||
|
|
||||||
|
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ls.wlBuffers[slot] = wlBuffer
|
||||||
|
|
||||||
|
s := slot
|
||||||
|
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||||
|
ls.slotBusy[s] = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ls.oldPool = ls.wlPool
|
ls.slotBusy[slot] = true
|
||||||
ls.oldBuffer = ls.wlBuffer
|
|
||||||
ls.wlPool = nil
|
|
||||||
ls.wlBuffer = nil
|
|
||||||
|
|
||||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ls.wlPool = pool
|
|
||||||
|
|
||||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ls.wlBuffer = wlBuffer
|
|
||||||
|
|
||||||
lsRef := ls
|
|
||||||
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
|
||||||
lsRef.bufferBusy = false
|
|
||||||
})
|
|
||||||
ls.bufferBusy = true
|
|
||||||
|
|
||||||
logicalW, logicalH := ls.state.LogicalSize()
|
logicalW, logicalH := ls.state.LogicalSize()
|
||||||
if logicalW == 0 || logicalH == 0 {
|
if logicalW == 0 || logicalH == 0 {
|
||||||
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
}
|
}
|
||||||
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
}
|
}
|
||||||
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
||||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
_ = ls.wlSurface.Commit()
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
p.redrawSurface(p.activeSurface)
|
p.activeSurface.needsRedraw = true
|
||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||||
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||||
p.redrawSurface(p.activeSurface)
|
p.activeSurface.needsRedraw = true
|
||||||
})
|
})
|
||||||
|
|
||||||
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||||
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
|
|||||||
if ls.scopyBuffer != nil {
|
if ls.scopyBuffer != nil {
|
||||||
ls.scopyBuffer.Destroy()
|
ls.scopyBuffer.Destroy()
|
||||||
}
|
}
|
||||||
if ls.oldBuffer != nil {
|
for i := range ls.wlBuffers {
|
||||||
ls.oldBuffer.Destroy()
|
if ls.wlBuffers[i] != nil {
|
||||||
}
|
ls.wlBuffers[i].Destroy()
|
||||||
if ls.oldPool != nil {
|
}
|
||||||
ls.oldPool.Destroy()
|
if ls.wlPools[i] != nil {
|
||||||
}
|
ls.wlPools[i].Destroy()
|
||||||
if ls.wlBuffer != nil {
|
}
|
||||||
ls.wlBuffer.Destroy()
|
|
||||||
}
|
|
||||||
if ls.wlPool != nil {
|
|
||||||
ls.wlPool.Destroy()
|
|
||||||
}
|
}
|
||||||
if ls.viewport != nil {
|
if ls.viewport != nil {
|
||||||
ls.viewport.Destroy()
|
ls.viewport.Destroy()
|
||||||
|
|||||||
@@ -274,6 +274,12 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
|
|||||||
return s.renderBufs[s.front]
|
return s.renderBufs[s.front]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) FrontIndex() int {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.front
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) SwapBuffers() {
|
func (s *SurfaceState) SwapBuffers() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.front ^= 1
|
s.front ^= 1
|
||||||
|
|||||||
@@ -62,12 +62,31 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
|
// Primary config file paths used to detect fresh installs.
|
||||||
|
configPrimaryPaths := map[string]string{
|
||||||
|
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
|
"Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
|
"Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||||
|
"Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||||
|
"Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||||
|
}
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
if replaceConfigs == nil {
|
if replaceConfigs == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
replace, exists := replaceConfigs[configType]
|
replace, exists := replaceConfigs[configType]
|
||||||
return !exists || replace
|
if !exists || replace {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Config is explicitly set to "don't replace" — but still deploy
|
||||||
|
// if the config file doesn't exist yet (fresh install scenario).
|
||||||
|
if primaryPath, ok := configPrimaryPaths[configType]; ok {
|
||||||
|
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch wm {
|
switch wm {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -624,3 +625,168 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
|
|||||||
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
||||||
|
allFalse := map[string]bool{
|
||||||
|
"Niri": false,
|
||||||
|
"Hyprland": false,
|
||||||
|
"Ghostty": false,
|
||||||
|
"Kitty": false,
|
||||||
|
"Alacritty": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("replaceConfigs nil deploys config", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-replace-nil-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerNiri,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil, // installedDeps
|
||||||
|
nil, // replaceConfigs
|
||||||
|
nil, // reinstallItems
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// With replaceConfigs=nil, all configs should be deployed
|
||||||
|
hasDeployed := false
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Deployed {
|
||||||
|
hasDeployed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasDeployed, "expected at least one config to be deployed when replaceConfigs is nil")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaceConfigs all false and config missing deploys config", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-replace-missing-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerNiri,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil, // installedDeps
|
||||||
|
allFalse, // replaceConfigs — all false
|
||||||
|
nil, // reinstallItems
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Config files don't exist on disk, so they should still be deployed
|
||||||
|
hasDeployed := false
|
||||||
|
for _, r := range results {
|
||||||
|
if r.Deployed {
|
||||||
|
hasDeployed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, hasDeployed, "expected configs to be deployed when files are missing, even with replaceConfigs all false")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaceConfigs false and config exists skips config", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-replace-exists-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
// Create the Ghostty primary config file so shouldReplaceConfig returns false
|
||||||
|
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
|
||||||
|
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Also create the Niri primary config file
|
||||||
|
niriPath := filepath.Join(tempDir, ".config", "niri", "config.kdl")
|
||||||
|
err = os.MkdirAll(filepath.Dir(niriPath), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(niriPath, []byte("// existing niri config\n"), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerNiri,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil, // installedDeps
|
||||||
|
allFalse, // replaceConfigs — all false
|
||||||
|
nil, // reinstallItems
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Both Niri and Ghostty config files exist, so with all false they should be skipped
|
||||||
|
for _, r := range results {
|
||||||
|
assert.Fail(t, "expected no configs to be deployed", "got deployed config: %s", r.ConfigType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replaceConfigs true and config exists deploys config", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-replace-true-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
// Create the Ghostty primary config file
|
||||||
|
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
|
||||||
|
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
replaceConfigs := map[string]bool{
|
||||||
|
"Niri": false,
|
||||||
|
"Hyprland": false,
|
||||||
|
"Ghostty": true, // explicitly true
|
||||||
|
"Kitty": false,
|
||||||
|
"Alacritty": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerNiri,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil, // installedDeps
|
||||||
|
replaceConfigs, // Ghostty=true, rest=false
|
||||||
|
nil, // reinstallItems
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Ghostty should be deployed because replaceConfigs["Ghostty"]=true
|
||||||
|
foundGhostty := false
|
||||||
|
for _, r := range results {
|
||||||
|
if r.ConfigType == "Ghostty" && r.Deployed {
|
||||||
|
foundGhostty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
|||||||
|
|
||||||
# === Sizing & Layout ===
|
# === Sizing & Layout ===
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100%
|
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
|||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
|
|
||||||
|
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
|
match app-id=r#"^org\.gnome\.Calculator$"#
|
||||||
match app-id=r#"^gnome-calculator$"#
|
match app-id=r#"^gnome-calculator$"#
|
||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
|
|||||||
@@ -242,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.packageInstalled("dms-shell-bin") {
|
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -328,6 +324,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
|
||||||
|
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
|
||||||
|
}
|
||||||
|
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: System Packages
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -445,6 +448,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if a.packageInstalled("quickshell-git") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.packageInstalled("quickshell") {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Removing stable quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||||
|
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||||
|
}
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: "Building quickshell-git before system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
||||||
|
}
|
||||||
|
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -453,6 +487,9 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||||
|
if slices.Contains(packages, "dms-shell") {
|
||||||
|
args = append(args, "--assume-installed", "dms-shell-compositor=1")
|
||||||
|
}
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -540,7 +577,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
var dmsShell []string
|
var dmsShell []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -621,7 +658,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -644,15 +681,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
if pkg == "dms-shell-bin" {
|
{
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
@@ -739,42 +768,9 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
|||||||
CommandInfo: "sudo pacman -U built-package",
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
|
||||||
var files []string
|
var files []string
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
// For DMS split packages, install base package
|
files = matches
|
||||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err == nil {
|
|
||||||
for _, match := range matches {
|
|
||||||
basename := filepath.Base(match)
|
|
||||||
// Always include base package
|
|
||||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
|
||||||
files = append(files, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also update compositor-specific packages if they're installed
|
|
||||||
if strings.HasSuffix(pkg, "-git") {
|
|
||||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
|
||||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
|
||||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
|
||||||
files = append(files, hyprlandMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if a.packageInstalled("dms-shell-niri-git") {
|
|
||||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
|
||||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
|
||||||
files = append(files, niriMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For other packages, install all built packages
|
|
||||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
|
||||||
files = matches
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return fmt.Errorf("no package files found after building %s", pkg)
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
|
|||||||
@@ -0,0 +1,418 @@
|
|||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrConfirmationRequired is returned when --yes is not set and the user
|
||||||
|
// must explicitly confirm the operation.
|
||||||
|
var ErrConfirmationRequired = fmt.Errorf("confirmation required: pass --yes to proceed")
|
||||||
|
|
||||||
|
// validConfigNames maps lowercase CLI input to the deployer key used in
|
||||||
|
// replaceConfigs. Keep in sync with the config types checked by
|
||||||
|
// shouldReplaceConfig in deployer.go.
|
||||||
|
var validConfigNames = map[string]string{
|
||||||
|
"niri": "Niri",
|
||||||
|
"hyprland": "Hyprland",
|
||||||
|
"ghostty": "Ghostty",
|
||||||
|
"kitty": "Kitty",
|
||||||
|
"alacritty": "Alacritty",
|
||||||
|
}
|
||||||
|
|
||||||
|
// orderedConfigNames defines the canonical order for config names in output.
|
||||||
|
// Must be kept in sync with validConfigNames.
|
||||||
|
var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacritty"}
|
||||||
|
|
||||||
|
// Config holds all CLI parameters for unattended installation.
|
||||||
|
type Config struct {
|
||||||
|
Compositor string // "niri" or "hyprland"
|
||||||
|
Terminal string // "ghostty", "kitty", or "alacritty"
|
||||||
|
IncludeDeps []string
|
||||||
|
ExcludeDeps []string
|
||||||
|
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
|
||||||
|
ReplaceConfigsAll bool // deploy/replace all configurations
|
||||||
|
Yes bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runner orchestrates unattended (headless) installation.
|
||||||
|
type Runner struct {
|
||||||
|
cfg Config
|
||||||
|
logChan chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRunner creates a new headless runner.
|
||||||
|
func NewRunner(cfg Config) *Runner {
|
||||||
|
return &Runner{
|
||||||
|
cfg: cfg,
|
||||||
|
logChan: make(chan string, 1000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogChan returns the log channel for file logging.
|
||||||
|
func (r *Runner) GetLogChan() <-chan string {
|
||||||
|
return r.logChan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the full unattended installation flow.
|
||||||
|
func (r *Runner) Run() error {
|
||||||
|
r.log("Starting headless installation")
|
||||||
|
|
||||||
|
// 1. Parse compositor and terminal selections
|
||||||
|
wm, err := r.parseWindowManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal, err := r.parseTerminal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Build replace-configs map
|
||||||
|
replaceConfigs, err := r.buildReplaceConfigs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Detect OS
|
||||||
|
r.log("Detecting operating system...")
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("OS detection failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
|
||||||
|
return fmt.Errorf("unsupported distribution: %s %s", osInfo.PrettyName, osInfo.VersionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stdout, "Detected: %s (%s)\n", osInfo.PrettyName, osInfo.Architecture)
|
||||||
|
|
||||||
|
// 4. Create distribution instance
|
||||||
|
distro, err := distros.NewDistribution(osInfo.Distribution.ID, r.logChan)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize distribution: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Detect dependencies
|
||||||
|
r.log("Detecting dependencies...")
|
||||||
|
fmt.Fprintln(os.Stdout, "Detecting dependencies...")
|
||||||
|
dependencies, err := distro.DetectDependenciesWithTerminal(context.Background(), wm, terminal)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dependency detection failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Apply include/exclude filters and build the disabled-items map.
|
||||||
|
// Headless mode does not currently collect any explicit reinstall selections,
|
||||||
|
// so keep reinstallItems nil instead of constructing an always-empty map.
|
||||||
|
disabledItems, err := r.buildDisabledItems(dependencies)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var reinstallItems map[string]bool
|
||||||
|
|
||||||
|
// Print dependency summary
|
||||||
|
fmt.Fprintln(os.Stdout, "\nDependencies:")
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
marker := " "
|
||||||
|
status := ""
|
||||||
|
if disabledItems[dep.Name] {
|
||||||
|
marker = " SKIP "
|
||||||
|
status = "(disabled)"
|
||||||
|
} else {
|
||||||
|
switch dep.Status {
|
||||||
|
case deps.StatusInstalled:
|
||||||
|
marker = " OK "
|
||||||
|
status = "(installed)"
|
||||||
|
case deps.StatusMissing:
|
||||||
|
marker = " NEW "
|
||||||
|
status = "(will install)"
|
||||||
|
case deps.StatusNeedsUpdate:
|
||||||
|
marker = " UPD "
|
||||||
|
status = "(will update)"
|
||||||
|
case deps.StatusNeedsReinstall:
|
||||||
|
marker = " RE "
|
||||||
|
status = "(will reinstall)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "%s%-30s %s\n", marker, dep.Name, status)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout)
|
||||||
|
|
||||||
|
// 6b. Require explicit confirmation unless --yes is set
|
||||||
|
if !r.cfg.Yes {
|
||||||
|
if replaceConfigs == nil {
|
||||||
|
// --replace-configs-all
|
||||||
|
fmt.Fprintln(os.Stdout, "Packages will be installed and all configurations will be replaced.")
|
||||||
|
fmt.Fprintln(os.Stdout, "Existing config files will be backed up before replacement.")
|
||||||
|
} else if r.anyConfigEnabled(replaceConfigs) {
|
||||||
|
var names []string
|
||||||
|
for _, cliName := range orderedConfigNames {
|
||||||
|
deployerKey := validConfigNames[cliName]
|
||||||
|
if replaceConfigs[deployerKey] {
|
||||||
|
names = append(names, deployerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "Packages will be installed. The following configurations will be replaced (with backups): %s\n", strings.Join(names, ", "))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stdout, "Packages will be installed. No configurations will be deployed.")
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, "Re-run with --yes (-y) to proceed.")
|
||||||
|
r.log("Aborted: --yes not set")
|
||||||
|
return ErrConfirmationRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Authenticate sudo
|
||||||
|
sudoPassword, err := r.resolveSudoPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Install packages
|
||||||
|
fmt.Fprintln(os.Stdout, "Installing packages...")
|
||||||
|
r.log("Starting package installation")
|
||||||
|
|
||||||
|
progressChan := make(chan distros.InstallProgressMsg, 100)
|
||||||
|
|
||||||
|
installErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
defer close(progressChan)
|
||||||
|
installErr <- distro.InstallPackages(
|
||||||
|
context.Background(),
|
||||||
|
dependencies,
|
||||||
|
wm,
|
||||||
|
sudoPassword,
|
||||||
|
reinstallItems,
|
||||||
|
disabledItems,
|
||||||
|
false, // skipGlobalUseFlags
|
||||||
|
progressChan,
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Consume progress messages and print them
|
||||||
|
for msg := range progressChan {
|
||||||
|
if msg.Error != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", msg.Error)
|
||||||
|
} else if msg.Step != "" {
|
||||||
|
fmt.Fprintf(os.Stdout, " [%3.0f%%] %s\n", msg.Progress*100, msg.Step)
|
||||||
|
}
|
||||||
|
if msg.LogOutput != "" {
|
||||||
|
r.log(msg.LogOutput)
|
||||||
|
fmt.Fprintf(os.Stdout, " %s\n", msg.LogOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-installErr; err != nil {
|
||||||
|
return fmt.Errorf("package installation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Greeter setup (if dms-greeter was included)
|
||||||
|
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
|
||||||
|
compositorName := "niri"
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
compositorName = "Hyprland"
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
|
||||||
|
logFunc := func(line string) {
|
||||||
|
r.log(line)
|
||||||
|
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
|
||||||
|
}
|
||||||
|
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
|
||||||
|
// Non-fatal, matching TUI behavior
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Deploy configurations
|
||||||
|
fmt.Fprintln(os.Stdout, "Deploying configurations...")
|
||||||
|
r.log("Starting configuration deployment")
|
||||||
|
|
||||||
|
deployer := config.NewConfigDeployer(r.logChan)
|
||||||
|
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
|
||||||
|
context.Background(),
|
||||||
|
wm,
|
||||||
|
terminal,
|
||||||
|
dependencies,
|
||||||
|
replaceConfigs,
|
||||||
|
reinstallItems,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("configuration deployment failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Deployed {
|
||||||
|
msg := fmt.Sprintf(" Deployed: %s", result.ConfigType)
|
||||||
|
if result.BackupPath != "" {
|
||||||
|
msg += fmt.Sprintf(" (backup: %s)", result.BackupPath)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stdout, msg)
|
||||||
|
}
|
||||||
|
if result.Error != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " Error deploying %s: %v\n", result.ConfigType, result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, "\nInstallation complete!")
|
||||||
|
r.log("Headless installation completed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDisabledItems computes the set of dependencies that should be skipped
|
||||||
|
// during installation, applying the --include-deps and --exclude-deps filters.
|
||||||
|
// dms-greeter is disabled by default (opt-in), matching TUI behavior.
|
||||||
|
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
|
||||||
|
disabledItems := make(map[string]bool)
|
||||||
|
|
||||||
|
// dms-greeter is opt-in (disabled by default), matching TUI behavior
|
||||||
|
for i := range dependencies {
|
||||||
|
if dependencies[i].Name == "dms-greeter" {
|
||||||
|
disabledItems["dms-greeter"] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process --include-deps (enable items that are disabled by default)
|
||||||
|
for _, name := range r.cfg.IncludeDeps {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !r.depExists(dependencies, name) {
|
||||||
|
return nil, fmt.Errorf("--include-deps: unknown dependency %q", name)
|
||||||
|
}
|
||||||
|
delete(disabledItems, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process --exclude-deps (disable items)
|
||||||
|
for _, name := range r.cfg.ExcludeDeps {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !r.depExists(dependencies, name) {
|
||||||
|
return nil, fmt.Errorf("--exclude-deps: unknown dependency %q", name)
|
||||||
|
}
|
||||||
|
// Don't allow excluding DMS itself
|
||||||
|
if name == "dms (DankMaterialShell)" {
|
||||||
|
return nil, fmt.Errorf("--exclude-deps: cannot exclude required package %q", name)
|
||||||
|
}
|
||||||
|
disabledItems[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return disabledItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildReplaceConfigs converts the --replace-configs / --replace-configs-all
|
||||||
|
// flags into the map[string]bool consumed by the config deployer.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - nil when --replace-configs-all is set (deployer treats nil as "replace all")
|
||||||
|
// - a map with all known configs set to false when neither flag is set (deploy only if config file is missing on disk)
|
||||||
|
// - a map with requested configs true, all others false for --replace-configs
|
||||||
|
// - an error when both flags are set or an invalid config name is given
|
||||||
|
func (r *Runner) buildReplaceConfigs() (map[string]bool, error) {
|
||||||
|
hasSpecific := len(r.cfg.ReplaceConfigs) > 0
|
||||||
|
if hasSpecific && r.cfg.ReplaceConfigsAll {
|
||||||
|
return nil, fmt.Errorf("--replace-configs and --replace-configs-all are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.cfg.ReplaceConfigsAll {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map with all known configs explicitly set to false
|
||||||
|
result := make(map[string]bool, len(validConfigNames))
|
||||||
|
for _, cliName := range orderedConfigNames {
|
||||||
|
result[validConfigNames[cliName]] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable only the requested configs
|
||||||
|
for _, name := range r.cfg.ReplaceConfigs {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deployerKey, ok := validConfigNames[strings.ToLower(name)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("--replace-configs: unknown config %q; valid values: niri, hyprland, ghostty, kitty, alacritty", name)
|
||||||
|
}
|
||||||
|
result[deployerKey] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) log(message string) {
|
||||||
|
select {
|
||||||
|
case r.logChan <- message:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) parseWindowManager() (deps.WindowManager, error) {
|
||||||
|
switch strings.ToLower(r.cfg.Compositor) {
|
||||||
|
case "niri":
|
||||||
|
return deps.WindowManagerNiri, nil
|
||||||
|
case "hyprland":
|
||||||
|
return deps.WindowManagerHyprland, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid --compositor value %q: must be 'niri' or 'hyprland'", r.cfg.Compositor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) parseTerminal() (deps.Terminal, error) {
|
||||||
|
switch strings.ToLower(r.cfg.Terminal) {
|
||||||
|
case "ghostty":
|
||||||
|
return deps.TerminalGhostty, nil
|
||||||
|
case "kitty":
|
||||||
|
return deps.TerminalKitty, nil
|
||||||
|
case "alacritty":
|
||||||
|
return deps.TerminalAlacritty, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid --term value %q: must be 'ghostty', 'kitty', or 'alacritty'", r.cfg.Terminal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) resolveSudoPassword() (string, error) {
|
||||||
|
// Check if sudo credentials are cached (via sudo -v or NOPASSWD)
|
||||||
|
cmd := exec.Command("sudo", "-n", "true")
|
||||||
|
if err := cmd.Run(); err == nil {
|
||||||
|
r.log("sudo cache is valid, no password needed")
|
||||||
|
fmt.Fprintln(os.Stdout, "sudo: using cached credentials")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"sudo authentication required but no cached credentials found\n" +
|
||||||
|
"Options:\n" +
|
||||||
|
" 1. Run 'sudo -v' before dankinstall to cache credentials\n" +
|
||||||
|
" 2. Configure passwordless sudo for your user",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) anyConfigEnabled(m map[string]bool) bool {
|
||||||
|
for _, v := range m {
|
||||||
|
if v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) depExists(dependencies []deps.Dependency, name string) bool {
|
||||||
|
for _, dep := range dependencies {
|
||||||
|
if dep.Name == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
package headless
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseWindowManager(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want deps.WindowManager
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"niri lowercase", "niri", deps.WindowManagerNiri, false},
|
||||||
|
{"niri mixed case", "Niri", deps.WindowManagerNiri, false},
|
||||||
|
{"hyprland lowercase", "hyprland", deps.WindowManagerHyprland, false},
|
||||||
|
{"hyprland mixed case", "Hyprland", deps.WindowManagerHyprland, false},
|
||||||
|
{"invalid", "sway", 0, true},
|
||||||
|
{"empty", "", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := NewRunner(Config{Compositor: tt.input})
|
||||||
|
got, err := r.parseWindowManager()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseWindowManager() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr && got != tt.want {
|
||||||
|
t.Errorf("parseWindowManager() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTerminal(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want deps.Terminal
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ghostty lowercase", "ghostty", deps.TerminalGhostty, false},
|
||||||
|
{"ghostty mixed case", "Ghostty", deps.TerminalGhostty, false},
|
||||||
|
{"kitty lowercase", "kitty", deps.TerminalKitty, false},
|
||||||
|
{"alacritty lowercase", "alacritty", deps.TerminalAlacritty, false},
|
||||||
|
{"alacritty uppercase", "ALACRITTY", deps.TerminalAlacritty, false},
|
||||||
|
{"invalid", "wezterm", 0, true},
|
||||||
|
{"empty", "", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := NewRunner(Config{Terminal: tt.input})
|
||||||
|
got, err := r.parseTerminal()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("parseTerminal() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr && got != tt.want {
|
||||||
|
t.Errorf("parseTerminal() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDepExists(t *testing.T) {
|
||||||
|
dependencies := []deps.Dependency{
|
||||||
|
{Name: "niri", Status: deps.StatusInstalled},
|
||||||
|
{Name: "ghostty", Status: deps.StatusMissing},
|
||||||
|
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
|
||||||
|
{Name: "dms-greeter", Status: deps.StatusMissing},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dep string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"existing dep", "niri", true},
|
||||||
|
{"existing dep with special chars", "dms (DankMaterialShell)", true},
|
||||||
|
{"existing optional dep", "dms-greeter", true},
|
||||||
|
{"non-existing dep", "firefox", false},
|
||||||
|
{"empty name", "", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewRunner(Config{})
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := r.depExists(dependencies, tt.dep); got != tt.want {
|
||||||
|
t.Errorf("depExists(%q) = %v, want %v", tt.dep, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRunner(t *testing.T) {
|
||||||
|
cfg := Config{
|
||||||
|
Compositor: "niri",
|
||||||
|
Terminal: "ghostty",
|
||||||
|
IncludeDeps: []string{"dms-greeter"},
|
||||||
|
ExcludeDeps: []string{"some-pkg"},
|
||||||
|
Yes: true,
|
||||||
|
}
|
||||||
|
r := NewRunner(cfg)
|
||||||
|
|
||||||
|
if r == nil {
|
||||||
|
t.Fatal("NewRunner returned nil")
|
||||||
|
}
|
||||||
|
if r.cfg.Compositor != "niri" {
|
||||||
|
t.Errorf("cfg.Compositor = %q, want %q", r.cfg.Compositor, "niri")
|
||||||
|
}
|
||||||
|
if r.cfg.Terminal != "ghostty" {
|
||||||
|
t.Errorf("cfg.Terminal = %q, want %q", r.cfg.Terminal, "ghostty")
|
||||||
|
}
|
||||||
|
if !r.cfg.Yes {
|
||||||
|
t.Error("cfg.Yes = false, want true")
|
||||||
|
}
|
||||||
|
if r.logChan == nil {
|
||||||
|
t.Error("logChan is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetLogChan(t *testing.T) {
|
||||||
|
r := NewRunner(Config{})
|
||||||
|
ch := r.GetLogChan()
|
||||||
|
if ch == nil {
|
||||||
|
t.Fatal("GetLogChan returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the channel is readable by sending a message
|
||||||
|
go func() {
|
||||||
|
r.logChan <- "test message"
|
||||||
|
}()
|
||||||
|
msg := <-ch
|
||||||
|
if msg != "test message" {
|
||||||
|
t.Errorf("received %q, want %q", msg, "test message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLog(t *testing.T) {
|
||||||
|
r := NewRunner(Config{})
|
||||||
|
|
||||||
|
// log should not block even if channel is full
|
||||||
|
for i := 0; i < 1100; i++ {
|
||||||
|
r.log("message")
|
||||||
|
}
|
||||||
|
// If we reach here without hanging, the non-blocking send works
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunRequiresYes(t *testing.T) {
|
||||||
|
// Verify that ErrConfirmationRequired is a distinct sentinel error
|
||||||
|
if ErrConfirmationRequired == nil {
|
||||||
|
t.Fatal("ErrConfirmationRequired should not be nil")
|
||||||
|
}
|
||||||
|
expected := "confirmation required: pass --yes to proceed"
|
||||||
|
if ErrConfirmationRequired.Error() != expected {
|
||||||
|
t.Errorf("ErrConfirmationRequired = %q, want %q", ErrConfirmationRequired.Error(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigYesStoredCorrectly(t *testing.T) {
|
||||||
|
// Yes=false (default) should be stored
|
||||||
|
rNo := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: false})
|
||||||
|
if rNo.cfg.Yes {
|
||||||
|
t.Error("cfg.Yes = true, want false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes=true should be stored
|
||||||
|
rYes := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: true})
|
||||||
|
if !rYes.cfg.Yes {
|
||||||
|
t.Error("cfg.Yes = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidConfigNamesCompleteness(t *testing.T) {
|
||||||
|
// orderedConfigNames and validConfigNames must stay in sync.
|
||||||
|
if len(orderedConfigNames) != len(validConfigNames) {
|
||||||
|
t.Fatalf("orderedConfigNames has %d entries but validConfigNames has %d",
|
||||||
|
len(orderedConfigNames), len(validConfigNames))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every entry in orderedConfigNames must exist in validConfigNames.
|
||||||
|
for _, name := range orderedConfigNames {
|
||||||
|
if _, ok := validConfigNames[name]; !ok {
|
||||||
|
t.Errorf("orderedConfigNames contains %q which is missing from validConfigNames", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validConfigNames must have no extra keys not in orderedConfigNames.
|
||||||
|
ordered := make(map[string]bool, len(orderedConfigNames))
|
||||||
|
for _, name := range orderedConfigNames {
|
||||||
|
ordered[name] = true
|
||||||
|
}
|
||||||
|
for key := range validConfigNames {
|
||||||
|
if !ordered[key] {
|
||||||
|
t.Errorf("validConfigNames contains %q which is missing from orderedConfigNames", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildReplaceConfigs(t *testing.T) {
|
||||||
|
allDeployerKeys := []string{"Niri", "Hyprland", "Ghostty", "Kitty", "Alacritty"}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
replaceConfigs []string
|
||||||
|
replaceAll bool
|
||||||
|
wantNil bool // expect nil (replace all)
|
||||||
|
wantEnabled []string // deployer keys that should be true
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "neither flag set",
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: nil, // all should be false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "replace-configs-all",
|
||||||
|
replaceAll: true,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "specific configs",
|
||||||
|
replaceConfigs: []string{"niri", "ghostty"},
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: []string{"Niri", "Ghostty"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both flags set",
|
||||||
|
replaceConfigs: []string{"niri"},
|
||||||
|
replaceAll: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid config name",
|
||||||
|
replaceConfigs: []string{"foo"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case insensitive",
|
||||||
|
replaceConfigs: []string{"NIRI", "Ghostty"},
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: []string{"Niri", "Ghostty"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single config",
|
||||||
|
replaceConfigs: []string{"kitty"},
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: []string{"Kitty"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace entry",
|
||||||
|
replaceConfigs: []string{" ", "niri"},
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: []string{"Niri"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate entry",
|
||||||
|
replaceConfigs: []string{"niri", "niri"},
|
||||||
|
wantNil: false,
|
||||||
|
wantEnabled: []string{"Niri"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := NewRunner(Config{
|
||||||
|
ReplaceConfigs: tt.replaceConfigs,
|
||||||
|
ReplaceConfigsAll: tt.replaceAll,
|
||||||
|
})
|
||||||
|
got, err := r.buildReplaceConfigs()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("buildReplaceConfigs() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.wantNil {
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("buildReplaceConfigs() = %v, want nil", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("buildReplaceConfigs() = nil, want non-nil map")
|
||||||
|
}
|
||||||
|
|
||||||
|
// All known deployer keys must be present
|
||||||
|
for _, key := range allDeployerKeys {
|
||||||
|
if _, exists := got[key]; !exists {
|
||||||
|
t.Errorf("missing deployer key %q in result map", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build enabled set for easy lookup
|
||||||
|
enabledSet := make(map[string]bool)
|
||||||
|
for _, k := range tt.wantEnabled {
|
||||||
|
enabledSet[k] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range allDeployerKeys {
|
||||||
|
want := enabledSet[key]
|
||||||
|
if got[key] != want {
|
||||||
|
t.Errorf("replaceConfigs[%q] = %v, want %v", key, got[key], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
|
||||||
|
r := NewRunner(Config{
|
||||||
|
Compositor: "niri",
|
||||||
|
Terminal: "ghostty",
|
||||||
|
ReplaceConfigs: []string{"niri", "ghostty"},
|
||||||
|
ReplaceConfigsAll: false,
|
||||||
|
})
|
||||||
|
if len(r.cfg.ReplaceConfigs) != 2 {
|
||||||
|
t.Errorf("len(ReplaceConfigs) = %d, want 2", len(r.cfg.ReplaceConfigs))
|
||||||
|
}
|
||||||
|
if r.cfg.ReplaceConfigsAll {
|
||||||
|
t.Error("ReplaceConfigsAll = true, want false")
|
||||||
|
}
|
||||||
|
|
||||||
|
r2 := NewRunner(Config{
|
||||||
|
Compositor: "niri",
|
||||||
|
Terminal: "ghostty",
|
||||||
|
ReplaceConfigsAll: true,
|
||||||
|
})
|
||||||
|
if !r2.cfg.ReplaceConfigsAll {
|
||||||
|
t.Error("ReplaceConfigsAll = false, want true")
|
||||||
|
}
|
||||||
|
if len(r2.cfg.ReplaceConfigs) != 0 {
|
||||||
|
t.Errorf("len(ReplaceConfigs) = %d, want 0", len(r2.cfg.ReplaceConfigs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildDisabledItems(t *testing.T) {
|
||||||
|
dependencies := []deps.Dependency{
|
||||||
|
{Name: "niri", Status: deps.StatusInstalled},
|
||||||
|
{Name: "ghostty", Status: deps.StatusMissing},
|
||||||
|
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
|
||||||
|
{Name: "dms-greeter", Status: deps.StatusMissing},
|
||||||
|
{Name: "waybar", Status: deps.StatusMissing},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
includeDeps []string
|
||||||
|
excludeDeps []string
|
||||||
|
deps []deps.Dependency // nil means use the shared fixture
|
||||||
|
wantErr bool
|
||||||
|
errContains string // substring expected in error message
|
||||||
|
wantDisabled []string // dep names that should be in disabledItems
|
||||||
|
wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no flags set, dms-greeter disabled by default",
|
||||||
|
wantDisabled: []string{"dms-greeter"},
|
||||||
|
wantEnabled: []string{"niri", "ghostty", "waybar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include dms-greeter enables it",
|
||||||
|
includeDeps: []string{"dms-greeter"},
|
||||||
|
wantEnabled: []string{"dms-greeter"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude a regular dep",
|
||||||
|
excludeDeps: []string{"waybar"},
|
||||||
|
wantDisabled: []string{"dms-greeter", "waybar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include unknown dep returns error",
|
||||||
|
includeDeps: []string{"nonexistent"},
|
||||||
|
wantErr: true,
|
||||||
|
errContains: "--include-deps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude unknown dep returns error",
|
||||||
|
excludeDeps: []string{"nonexistent"},
|
||||||
|
wantErr: true,
|
||||||
|
errContains: "--exclude-deps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exclude DMS itself is forbidden",
|
||||||
|
excludeDeps: []string{"dms (DankMaterialShell)"},
|
||||||
|
wantErr: true,
|
||||||
|
errContains: "cannot exclude required package",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include and exclude same dep",
|
||||||
|
includeDeps: []string{"dms-greeter"},
|
||||||
|
excludeDeps: []string{"dms-greeter"},
|
||||||
|
wantDisabled: []string{"dms-greeter"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace entries are skipped",
|
||||||
|
includeDeps: []string{" ", "dms-greeter"},
|
||||||
|
wantEnabled: []string{"dms-greeter"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no dms-greeter in deps, nothing disabled by default",
|
||||||
|
deps: []deps.Dependency{
|
||||||
|
{Name: "niri", Status: deps.StatusInstalled},
|
||||||
|
},
|
||||||
|
wantEnabled: []string{"niri"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := NewRunner(Config{
|
||||||
|
IncludeDeps: tt.includeDeps,
|
||||||
|
ExcludeDeps: tt.excludeDeps,
|
||||||
|
})
|
||||||
|
d := tt.deps
|
||||||
|
if d == nil {
|
||||||
|
d = dependencies
|
||||||
|
}
|
||||||
|
got, err := r.buildDisabledItems(d)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("buildDisabledItems() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if tt.wantErr {
|
||||||
|
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||||
|
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("buildDisabledItems() returned nil map, want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expected disabled items
|
||||||
|
for _, name := range tt.wantDisabled {
|
||||||
|
if !got[name] {
|
||||||
|
t.Errorf("expected %q to be disabled, but it is not", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check expected enabled items (should not be in the map or be false)
|
||||||
|
for _, name := range tt.wantEnabled {
|
||||||
|
if got[name] {
|
||||||
|
t.Errorf("expected %q to NOT be disabled, but it is", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If wantDisabled is empty, the map should have length 0
|
||||||
|
if len(tt.wantDisabled) == 0 && len(got) != 0 {
|
||||||
|
t.Errorf("expected empty disabledItems map, got %v", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/sblinch/kdl-go"
|
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,7 +291,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
parser.currentSource = overridePath
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,103 @@ type NiriParser struct {
|
|||||||
conflictingConfigs map[string]*NiriKeyBinding
|
conflictingConfigs map[string]*NiriKeyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseKDL(data []byte) (*document.Document, error) {
|
||||||
|
return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKDLBraces(input string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(input))
|
||||||
|
|
||||||
|
var prev byte
|
||||||
|
n := len(input)
|
||||||
|
for i := 0; i < n; {
|
||||||
|
c := input[i]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '"':
|
||||||
|
end := findStringEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '"'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '/':
|
||||||
|
end := findLineCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '\n'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '*':
|
||||||
|
end := findBlockCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '/'
|
||||||
|
i = end
|
||||||
|
case c == '{' && prev != 0 && !isBraceAdjacentSpace(prev):
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStringEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
for i := start + 1; i < n; {
|
||||||
|
switch s[i] {
|
||||||
|
case '\\':
|
||||||
|
i += 2
|
||||||
|
case '"':
|
||||||
|
return i + 1
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLineCommentEnd(s string, start int) int {
|
||||||
|
for i := start + 2; i < len(s); i++ {
|
||||||
|
if s[i] == '\n' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBlockCommentEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
depth := 1
|
||||||
|
for i := start + 2; i < n && depth > 0; {
|
||||||
|
switch {
|
||||||
|
case i+1 < n && s[i] == '/' && s[i+1] == '*':
|
||||||
|
depth++
|
||||||
|
i += 2
|
||||||
|
case i+1 < n && s[i] == '*' && s[i+1] == '/':
|
||||||
|
depth--
|
||||||
|
i += 2
|
||||||
|
if depth == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBraceAdjacentSpace(b byte) bool {
|
||||||
|
switch b {
|
||||||
|
case ' ', '\t', '\n', '\r', '{':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
@@ -91,7 +188,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,7 +256,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,74 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
||||||
|
config := `recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
Alt+grave { next-window filter="app-id"; }
|
||||||
|
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||||
|
Alt+Escape { next-window scope="all"; }
|
||||||
|
Alt+Shift+Escape{ previous-window scope="all"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed on valid niri config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var found *NiriKeyBinding
|
||||||
|
for i := range result.Section.Keybinds {
|
||||||
|
kb := &result.Section.Keybinds[i]
|
||||||
|
if kb.Key == "Escape" && slices.Contains(kb.Mods, "Alt") && slices.Contains(kb.Mods, "Shift") {
|
||||||
|
found = kb
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
t.Fatal("Alt+Shift+Escape bind missing — '{' without preceding space was not handled")
|
||||||
|
}
|
||||||
|
if found.Action != "previous-window" {
|
||||||
|
t.Errorf("Action = %q, want %q", found.Action, "previous-window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeKDLBraces(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"already spaced", "node { child }\n", "node { child }\n"},
|
||||||
|
{"missing space", "node{ child }\n", "node { child }\n"},
|
||||||
|
{"niri keybind", "Alt+Shift+Escape{ previous-window; }", "Alt+Shift+Escape { previous-window; }"},
|
||||||
|
{"brace inside string", `node "a{b" { child }`, `node "a{b" { child }`},
|
||||||
|
{"brace in line comment", "// foo{bar\nnode { }", "// foo{bar\nnode { }"},
|
||||||
|
{"brace in block comment", "/* foo{bar */ node{ }", "/* foo{bar */ node { }"},
|
||||||
|
{"escaped quote in string", `node "a\"b{c" { }`, `node "a\"b{c" { }`},
|
||||||
|
{"leading brace", "{ child }", "{ child }"},
|
||||||
|
{"nested missing space", "a{b{ c }}", "a {b { c }}"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := normalizeKDLBraces(tc.in)
|
||||||
|
if got != tc.out {
|
||||||
|
t.Errorf("normalizeKDLBraces(%q) = %q, want %q", tc.in, got, tc.out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriParseKeyCombo(t *testing.T) {
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
combo string
|
combo string
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,7 +22,16 @@ type FileLogger struct {
|
|||||||
|
|
||||||
func NewFileLogger() (*FileLogger, error) {
|
func NewFileLogger() (*FileLogger, error) {
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
logPath := fmt.Sprintf("/tmp/dankinstall-%d.log", timestamp)
|
|
||||||
|
// Use DANKINSTALL_LOG_DIR if set, otherwise fall back to /tmp.
|
||||||
|
logDir := os.Getenv("DANKINSTALL_LOG_DIR")
|
||||||
|
if logDir == "" {
|
||||||
|
logDir = "/tmp"
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||||
|
}
|
||||||
|
logPath := filepath.Join(logDir, fmt.Sprintf("dankinstall-%d.log", timestamp))
|
||||||
|
|
||||||
file, err := os.Create(logPath)
|
file, err := os.Create(logPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
func getAllOutputInfos() map[string]*outputInfo {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
state.y = pe.Y
|
||||||
})
|
})
|
||||||
|
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||||
|
state.scale = se.Scale
|
||||||
|
})
|
||||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*outputInfo, len(heads))
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == outputName {
|
if state.name == "" {
|
||||||
return &outputInfo{
|
continue
|
||||||
x: state.x,
|
}
|
||||||
y: state.y,
|
result[state.name] = &outputInfo{
|
||||||
transform: state.transform,
|
x: state.x,
|
||||||
}, true
|
y: state.y,
|
||||||
|
scale: state.scale,
|
||||||
|
transform: state.transform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return nil, false
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
infos := getAllOutputInfos()
|
||||||
|
if infos == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
info, ok := infos[outputName]
|
||||||
|
return info, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -304,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture all outputs first to get actual buffer sizes
|
wlrInfos := getAllOutputInfos()
|
||||||
type capturedOutput struct {
|
|
||||||
output *WaylandOutput
|
|
||||||
result *CaptureResult
|
|
||||||
physX int
|
|
||||||
physY int
|
|
||||||
}
|
|
||||||
captured := make([]capturedOutput, 0, len(outputs))
|
|
||||||
|
|
||||||
var minX, minY, maxX, maxY int
|
type pendingOutput struct {
|
||||||
first := true
|
result *CaptureResult
|
||||||
|
logX float64
|
||||||
|
logY float64
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
var pending []pendingOutput
|
||||||
|
maxScale := 1.0
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -328,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outX, outY := output.x, output.y
|
logX, logY := float64(output.x), float64(output.y)
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
outX, outY = hx, hy
|
logX, logY = float64(hx), float64(hy)
|
||||||
}
|
}
|
||||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||||
scale = s
|
scale = hs
|
||||||
}
|
}
|
||||||
case CompositorDWL:
|
default:
|
||||||
if info, ok := getOutputInfo(output.name); ok {
|
if wlrInfos != nil {
|
||||||
outX, outY = info.x, info.y
|
if info, ok := wlrInfos[output.name]; ok {
|
||||||
|
logX, logY = float64(info.x), float64(info.y)
|
||||||
|
if info.scale > 0 {
|
||||||
|
scale = info.scale
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
physX := int(float64(outX) * scale)
|
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||||
physY := int(float64(outY) * scale)
|
if scale > maxScale {
|
||||||
|
maxScale = scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
captured = append(captured, capturedOutput{
|
if len(pending) == 0 {
|
||||||
output: output,
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
result: result,
|
}
|
||||||
physX: physX,
|
if len(pending) == 1 {
|
||||||
physY: physY,
|
return pending[0].result, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
right := physX + result.Buffer.Width
|
type layoutEntry struct {
|
||||||
bottom := physY + result.Buffer.Height
|
result *CaptureResult
|
||||||
|
canvasX int
|
||||||
|
canvasY int
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
}
|
||||||
|
entries := make([]layoutEntry, len(pending))
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
|
||||||
if first {
|
for i, p := range pending {
|
||||||
minX, minY = physX, physY
|
cx := int(math.Round(p.logX * maxScale))
|
||||||
maxX, maxY = right, bottom
|
cy := int(math.Round(p.logY * maxScale))
|
||||||
first = false
|
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||||
|
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||||
|
|
||||||
|
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||||
|
|
||||||
|
right := cx + cw
|
||||||
|
bottom := cy + ch
|
||||||
|
if i == 0 {
|
||||||
|
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cx < minX {
|
||||||
if physX < minX {
|
minX = cx
|
||||||
minX = physX
|
|
||||||
}
|
}
|
||||||
if physY < minY {
|
if cy < minY {
|
||||||
minY = physY
|
minY = cy
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
maxX = right
|
||||||
@@ -381,35 +404,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(captured) == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to capture any outputs")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(captured) == 1 {
|
|
||||||
return captured[0].result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalW := maxX - minX
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||||
compositeStride := totalW * 4
|
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
c.result.Buffer.Close()
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = c.result.Format
|
format = e.result.Format
|
||||||
}
|
}
|
||||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
s.blitBufferScaled(composite, e.result.Buffer,
|
||||||
c.result.Buffer.Close()
|
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||||
|
e.result.YInverted)
|
||||||
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -419,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for srcY := 0; srcY < src.Height; srcY++ {
|
for dy := 0; dy < dstH; dy++ {
|
||||||
actualSrcY := srcY
|
canvasY := dstY + dy
|
||||||
if yInverted {
|
if canvasY < 0 || canvasY >= dst.Height {
|
||||||
actualSrcY = src.Height - 1 - srcY
|
|
||||||
}
|
|
||||||
|
|
||||||
dy := dstY + srcY
|
|
||||||
if dy < 0 || dy >= dst.Height {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := actualSrcY * src.Stride
|
srcY := dy * src.Height / dstH
|
||||||
dstRowOff := dy * dst.Stride
|
if yInverted {
|
||||||
|
srcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= src.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for srcX := 0; srcX < src.Width; srcX++ {
|
srcRowOff := srcY * src.Stride
|
||||||
dx := dstX + srcX
|
dstRowOff := canvasY * dst.Stride
|
||||||
if dx < 0 || dx >= dst.Width {
|
|
||||||
|
for dx := 0; dx < dstW; dx++ {
|
||||||
|
canvasX := dstX + dx
|
||||||
|
if canvasX < 0 || canvasX >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcX := dx * src.Width / dstW
|
||||||
|
if srcX >= src.Width {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + dx*4
|
di := dstRowOff + canvasX*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -215,31 +215,34 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if timer, exists := b.debounceTimers[id]; exists {
|
if existing, exists := b.debounceTimers[id]; exists {
|
||||||
timer.Reset(200 * time.Millisecond)
|
if existing.Stop() {
|
||||||
} else {
|
b.debounceWg.Done()
|
||||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
}
|
||||||
b.debounceMutex.Lock()
|
|
||||||
pending, exists := b.debouncePending[id]
|
|
||||||
if exists {
|
|
||||||
delete(b.debouncePending, id)
|
|
||||||
}
|
|
||||||
b.debounceMutex.Unlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.setBrightnessImmediateWithExponent(id, pending.percent)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pending.callback != nil {
|
|
||||||
pending.callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.debounceWg.Add(1)
|
||||||
|
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||||
|
defer b.debounceWg.Done()
|
||||||
|
|
||||||
|
b.debounceMutex.Lock()
|
||||||
|
pending, hasPending := b.debouncePending[id]
|
||||||
|
delete(b.debouncePending, id)
|
||||||
|
delete(b.debounceTimers, id)
|
||||||
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
|
if !hasPending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.setBrightnessImmediateWithExponent(id, pending.percent); err != nil {
|
||||||
|
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending.callback != nil {
|
||||||
|
pending.callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
b.debounceMutex.Unlock()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -490,5 +493,19 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
|||||||
return percent
|
return percent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) WaitPending() {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
b.debounceWg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Debug("WaitPending timed out waiting for DDC writes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) Close() {
|
func (b *DDCBackend) Close() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type DDCBackend struct {
|
|||||||
debounceMutex sync.Mutex
|
debounceMutex sync.Mutex
|
||||||
debounceTimers map[string]*time.Timer
|
debounceTimers map[string]*time.Timer
|
||||||
debouncePending map[string]ddcPendingSet
|
debouncePending map[string]ddcPendingSet
|
||||||
|
debounceWg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type ddcPendingSet struct {
|
type ddcPendingSet struct {
|
||||||
|
|||||||
@@ -158,18 +158,26 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID && bssid == currentBSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID && bssid == currentBSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,19 +522,27 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Hidden: hiddenSSIDs[ssid],
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,19 +1078,27 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := connected && apSSID == ssid
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: apSSID,
|
SSID: apSSID,
|
||||||
BSSID: apBSSID,
|
BSSID: apBSSID,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: connected && apSSID == ssid,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
Hidden: hiddenSSIDs[apSSID],
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Device: name,
|
Device: name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||||
@@ -72,6 +73,7 @@ var clipboardManager *clipboard.Manager
|
|||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
|
var trayRecoveryManager *trayrecovery.Manager
|
||||||
var locationManager *location.Manager
|
var locationManager *location.Manager
|
||||||
var geoClientInstance geolocation.Client
|
var geoClientInstance geolocation.Client
|
||||||
|
|
||||||
@@ -394,6 +396,18 @@ func InitializeThemeModeManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitializeTrayRecoveryManager() error {
|
||||||
|
manager, err := trayrecovery.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
trayRecoveryManager = manager
|
||||||
|
|
||||||
|
log.Info("TrayRecovery manager initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func InitializeLocationManager(geoClient geolocation.Client) error {
|
func InitializeLocationManager(geoClient geolocation.Client) error {
|
||||||
manager, err := location.NewManager(geoClient)
|
manager, err := location.NewManager(geoClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1325,6 +1339,9 @@ func cleanupManagers() {
|
|||||||
if themeModeManager != nil {
|
if themeModeManager != nil {
|
||||||
themeModeManager.Close()
|
themeModeManager.Close()
|
||||||
}
|
}
|
||||||
|
if trayRecoveryManager != nil {
|
||||||
|
trayRecoveryManager.Close()
|
||||||
|
}
|
||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
@@ -1610,6 +1627,18 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
if loginctlManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := InitializeTrayRecoveryManager(); err != nil {
|
||||||
|
log.Warnf("TrayRecovery manager unavailable: %v", err)
|
||||||
|
} else {
|
||||||
|
trayRecoveryManager.WatchLoginctl(loginctlManager)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
geoClient := geolocation.NewClient()
|
geoClient := geolocation.NewClient()
|
||||||
geoClientInstance = geoClient
|
geoClientInstance = geoClient
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package trayrecovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const resumeDelay = 3 * time.Second
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
conn *dbus.Conn
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() (*Manager, error) {
|
||||||
|
conn, err := dbus.ConnectSessionBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &Manager{
|
||||||
|
conn: conn,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run a startup scan when the system has been suspended at least once.
|
||||||
|
// On a fresh boot CLOCK_BOOTTIME ≈ CLOCK_MONOTONIC (difference ~0).
|
||||||
|
// After any suspend/resume cycle the difference grows by the time spent
|
||||||
|
// sleeping. This avoids duplicate registrations on normal boot where apps
|
||||||
|
// are still starting up and will register their own tray icons shortly.
|
||||||
|
if timeSuspended() > 5*time.Second {
|
||||||
|
go m.scheduleRecovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchLoginctl subscribes to loginctl session state changes and triggers
|
||||||
|
// tray recovery after resume from suspend (PrepareForSleep false transition).
|
||||||
|
// This handles the case where the process survives suspend.
|
||||||
|
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||||
|
ch := lm.Subscribe("tray-recovery")
|
||||||
|
m.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
defer lm.Unsubscribe("tray-recovery")
|
||||||
|
|
||||||
|
wasSleeping := false
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
case state, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.PreparingForSleep {
|
||||||
|
wasSleeping = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if wasSleeping {
|
||||||
|
wasSleeping = false
|
||||||
|
go m.scheduleRecovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) scheduleRecovery() {
|
||||||
|
select {
|
||||||
|
case <-time.After(resumeDelay):
|
||||||
|
m.recoverTrayItems()
|
||||||
|
case <-m.stopChan:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Close() {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
close(m.stopChan)
|
||||||
|
}
|
||||||
|
m.wg.Wait()
|
||||||
|
if m.conn != nil {
|
||||||
|
m.conn.Close()
|
||||||
|
}
|
||||||
|
log.Info("TrayRecovery manager closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||||
|
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||||
|
// CLOCK_MONOTONIC (excludes suspend).
|
||||||
|
func timeSuspended() time.Duration {
|
||||||
|
var bt, mt unix.Timespec
|
||||||
|
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||||
|
if diff < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(diff)
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
package trayrecovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sniWatcherDest = "org.kde.StatusNotifierWatcher"
|
||||||
|
sniWatcherPath = "/StatusNotifierWatcher"
|
||||||
|
sniWatcherIface = "org.kde.StatusNotifierWatcher"
|
||||||
|
sniItemIface = "org.kde.StatusNotifierItem"
|
||||||
|
dbusIface = "org.freedesktop.DBus"
|
||||||
|
propsIface = "org.freedesktop.DBus.Properties"
|
||||||
|
probeTimeout = 300 * time.Millisecond
|
||||||
|
connProbeTimeout = 150 * time.Millisecond
|
||||||
|
batchSize = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
var excludedPrefixes = []string{
|
||||||
|
"org.freedesktop.",
|
||||||
|
"org.gnome.",
|
||||||
|
"org.kde.StatusNotifier",
|
||||||
|
"com.canonical.AppMenu",
|
||||||
|
"org.mpris.",
|
||||||
|
"org.pipewire.",
|
||||||
|
"org.pulseaudio",
|
||||||
|
"fi.epitaph",
|
||||||
|
"quickshell",
|
||||||
|
"org.kde.quickshell",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) recoverTrayItems() {
|
||||||
|
registeredItems := m.getRegisteredItems()
|
||||||
|
allNames := m.getDBusNames()
|
||||||
|
if allNames == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registeredConnIDs := m.buildRegisteredConnIDs(registeredItems)
|
||||||
|
|
||||||
|
count := len(registeredItems)
|
||||||
|
log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count)
|
||||||
|
|
||||||
|
m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs)
|
||||||
|
m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getRegisteredItems() []string {
|
||||||
|
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||||
|
variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems")
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("TrayRecoveryService: failed to get registered items: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := variant.Value().(type) {
|
||||||
|
case []string:
|
||||||
|
return v
|
||||||
|
case []any:
|
||||||
|
items := make([]string, 0, len(v))
|
||||||
|
for _, elem := range v {
|
||||||
|
if s, ok := elem.(string); ok {
|
||||||
|
items = append(items, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getDBusNames() []string {
|
||||||
|
var names []string
|
||||||
|
err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("TrayRecoveryService: failed to list bus names: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getNameOwner(name string) string {
|
||||||
|
var owner string
|
||||||
|
err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return owner
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRegisteredConnIDs resolves every registered SNI item (well-known name
|
||||||
|
// or :1.xxx connection ID) to a canonical connection ID. This prevents
|
||||||
|
// duplicates in both directions.
|
||||||
|
func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool {
|
||||||
|
connIDs := make(map[string]bool, len(registeredItems))
|
||||||
|
for _, item := range registeredItems {
|
||||||
|
name := extractName(item)
|
||||||
|
if strings.HasPrefix(name, ":1.") {
|
||||||
|
connIDs[name] = true
|
||||||
|
} else {
|
||||||
|
owner := m.getNameOwner(name)
|
||||||
|
if owner != "" {
|
||||||
|
connIDs[owner] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for
|
||||||
|
// unregistered SNI items and re-registers them.
|
||||||
|
func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||||
|
registeredRaw := strings.Join(registeredItems, "\n")
|
||||||
|
|
||||||
|
for _, name := range allNames {
|
||||||
|
if strings.HasPrefix(name, ":") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(registeredRaw, name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if this name's connection ID is already in the registered set
|
||||||
|
// (handles the case where the app registered via connection ID instead)
|
||||||
|
connForName := m.getNameOwner(name)
|
||||||
|
if connForName != "" && registeredConnIDs[connForName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isExcludedName(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
short := shortName(name)
|
||||||
|
objectPaths := []string{
|
||||||
|
"/StatusNotifierItem",
|
||||||
|
"/org/ayatana/NotificationItem/" + short,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, objPath := range objectPaths {
|
||||||
|
if m.probeSNI(name, objPath, probeTimeout) {
|
||||||
|
m.registerSNI(name)
|
||||||
|
// Update set so the connection-ID section won't double-register this app
|
||||||
|
if connForName != "" {
|
||||||
|
registeredConnIDs[connForName] = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered
|
||||||
|
// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an
|
||||||
|
// error instantly, so this is fast.
|
||||||
|
func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||||
|
registeredRaw := strings.Join(registeredItems, "\n")
|
||||||
|
registeredLower := strings.ToLower(registeredRaw)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
sem := make(chan struct{}, batchSize)
|
||||||
|
|
||||||
|
for _, name := range allNames {
|
||||||
|
if !strings.HasPrefix(name, ":1.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if registeredConnIDs[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sem <- struct{}{}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(conn string) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
sniID := m.getSNIId(conn, connProbeTimeout)
|
||||||
|
if sniID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if an item with the same Id is already registered (case-insensitive)
|
||||||
|
if strings.Contains(registeredLower, strings.ToLower(sniID)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.registerSNI(conn)
|
||||||
|
log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID)
|
||||||
|
}(name)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
obj := m.conn.Object(dest, dbus.ObjectPath(path))
|
||||||
|
var props map[string]dbus.Variant
|
||||||
|
err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasID := props["Id"]
|
||||||
|
return hasID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getSNIId(dest string, timeout time.Duration) string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
obj := m.conn.Object(dest, "/StatusNotifierItem")
|
||||||
|
var variant dbus.Variant
|
||||||
|
err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := variant.Value().(string)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) registerSNI(name string) {
|
||||||
|
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||||
|
call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name)
|
||||||
|
if call.Err != nil {
|
||||||
|
log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("TrayRecovery: re-registered %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractName(item string) string {
|
||||||
|
if idx := strings.IndexByte(item, '/'); idx != -1 {
|
||||||
|
return item[:idx]
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortName(name string) string {
|
||||||
|
parts := strings.Split(name, ".")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExcludedName(name string) bool {
|
||||||
|
for _, prefix := range excludedPrefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -3,8 +3,11 @@ package wayland
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -73,7 +76,10 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
|||||||
m.post(func() {
|
m.post(func() {
|
||||||
log.Info("Gamma control enabled at startup")
|
log.Info("Gamma control enabled at startup")
|
||||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
|
m.availOutputsMu.RLock()
|
||||||
|
outs := slices.Clone(m.availableOutputs)
|
||||||
|
m.availOutputsMu.RUnlock()
|
||||||
|
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
|
||||||
log.Errorf("Failed to initialize gamma controls: %v", err)
|
log.Errorf("Failed to initialize gamma controls: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -170,6 +176,7 @@ func (m *Manager) setupRegistry() error {
|
|||||||
})
|
})
|
||||||
if gammaMgr != nil {
|
if gammaMgr != nil {
|
||||||
outputs = append(outputs, output)
|
outputs = append(outputs, output)
|
||||||
|
m.addAvailableOutput(output)
|
||||||
}
|
}
|
||||||
m.outputRegNames.Store(outputID, e.Name)
|
m.outputRegNames.Store(outputID, e.Name)
|
||||||
|
|
||||||
@@ -204,6 +211,11 @@ func (m *Manager) setupRegistry() error {
|
|||||||
}
|
}
|
||||||
if foundOut.gammaControl != nil {
|
if foundOut.gammaControl != nil {
|
||||||
foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
|
foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
|
||||||
|
foundOut.gammaControl = nil
|
||||||
|
}
|
||||||
|
m.removeAvailableOutput(foundOut.output)
|
||||||
|
if foundOut.output != nil && !foundOut.output.IsZombie() {
|
||||||
|
_ = foundOut.output.Release()
|
||||||
}
|
}
|
||||||
m.outputs.Delete(foundID)
|
m.outputs.Delete(foundID)
|
||||||
|
|
||||||
@@ -288,14 +300,28 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() {
|
||||||
|
ctrl.Destroy()
|
||||||
|
}
|
||||||
|
out.gammaControl = nil
|
||||||
out.failed = true
|
out.failed = true
|
||||||
out.rampSize = 0
|
out.rampSize = 0
|
||||||
out.retryCount++
|
out.retryCount++
|
||||||
out.lastFailTime = time.Now()
|
out.lastFailTime = time.Now()
|
||||||
|
|
||||||
|
if !m.outputStillValid(out) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
|
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
|
||||||
time.AfterFunc(backoff, func() {
|
time.AfterFunc(backoff, func() {
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
|
if !m.outputStillValid(out) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, stillTracked := m.outputs.Load(outputID); !stillTracked {
|
||||||
|
return
|
||||||
|
}
|
||||||
m.recreateOutputControl(out)
|
m.recreateOutputControl(out)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -303,12 +329,75 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) addAvailableOutput(o *wlclient.Output) {
|
||||||
|
if o == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.availOutputsMu.Lock()
|
||||||
|
defer m.availOutputsMu.Unlock()
|
||||||
|
if slices.Contains(m.availableOutputs, o) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.availableOutputs = append(m.availableOutputs, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) removeAvailableOutput(o *wlclient.Output) {
|
||||||
|
if o == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.availOutputsMu.Lock()
|
||||||
|
defer m.availOutputsMu.Unlock()
|
||||||
|
m.availableOutputs = slices.DeleteFunc(m.availableOutputs, func(existing *wlclient.Output) bool {
|
||||||
|
return existing == o
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) outputStillValid(out *outputState) bool {
|
||||||
|
switch {
|
||||||
|
case out == nil:
|
||||||
|
return false
|
||||||
|
case out.output == nil:
|
||||||
|
return false
|
||||||
|
case out.output.IsZombie():
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m.availOutputsMu.RLock()
|
||||||
|
defer m.availOutputsMu.RUnlock()
|
||||||
|
return slices.Contains(m.availableOutputs, out.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isConnectionDeadErr(err error) bool {
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return false
|
||||||
|
case errors.Is(err, syscall.EPIPE):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, syscall.ECONNRESET):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, syscall.EBADF):
|
||||||
|
return true
|
||||||
|
case errors.Is(err, io.EOF):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) addOutputControl(output *wlclient.Output) error {
|
func (m *Manager) addOutputControl(output *wlclient.Output) error {
|
||||||
|
switch {
|
||||||
|
case m.connectionDead.Load():
|
||||||
|
return nil
|
||||||
|
case output == nil || output.IsZombie():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
outputID := output.ID()
|
outputID := output.ID()
|
||||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
|
|
||||||
control, err := gammaMgr.GetGammaControl(output)
|
control, err := gammaMgr.GetGammaControl(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isConnectionDeadErr(err) {
|
||||||
|
m.markConnectionDead(err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,26 +418,37 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
|
|||||||
enabled := m.config.Enabled
|
enabled := m.config.Enabled
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
if !enabled || !m.controlsInitialized {
|
switch {
|
||||||
|
case m.connectionDead.Load():
|
||||||
|
return nil
|
||||||
|
case !enabled || !m.controlsInitialized:
|
||||||
|
return nil
|
||||||
|
case out.isVirtual:
|
||||||
|
return nil
|
||||||
|
case out.retryCount >= 10:
|
||||||
|
return nil
|
||||||
|
case !m.outputStillValid(out):
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, ok := m.outputs.Load(out.id); !ok {
|
if _, ok := m.outputs.Load(out.id); !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if out.isVirtual {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if out.retryCount >= 10 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("no gamma manager")
|
return fmt.Errorf("no gamma manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if existing, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && existing != nil && !existing.IsZombie() {
|
||||||
|
existing.Destroy()
|
||||||
|
out.gammaControl = nil
|
||||||
|
}
|
||||||
|
|
||||||
control, err := gammaMgr.GetGammaControl(out.output)
|
control, err := gammaMgr.GetGammaControl(out.output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if isConnectionDeadErr(err) {
|
||||||
|
m.markConnectionDead(err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +458,13 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) markConnectionDead(err error) {
|
||||||
|
if m.connectionDead.Swap(true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("gamma: wayland connection appears dead (%v); pausing gamma operations", err)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) recalcSchedule(now time.Time) {
|
func (m *Manager) recalcSchedule(now time.Time) {
|
||||||
m.configMutex.RLock()
|
m.configMutex.RLock()
|
||||||
config := m.config
|
config := m.config
|
||||||
@@ -690,11 +797,12 @@ func (m *Manager) applyGamma(temp int) {
|
|||||||
gamma := m.config.Gamma
|
gamma := m.config.Gamma
|
||||||
m.configMutex.RUnlock()
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
if !m.controlsInitialized {
|
switch {
|
||||||
|
case m.connectionDead.Load():
|
||||||
return
|
return
|
||||||
}
|
case !m.controlsInitialized:
|
||||||
|
return
|
||||||
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
|
case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,7 +822,14 @@ func (m *Manager) applyGamma(temp int) {
|
|||||||
var jobs []job
|
var jobs []job
|
||||||
|
|
||||||
for _, out := range outs {
|
for _, out := range outs {
|
||||||
if out.failed || out.rampSize == 0 {
|
switch {
|
||||||
|
case out.failed:
|
||||||
|
continue
|
||||||
|
case out.rampSize == 0:
|
||||||
|
continue
|
||||||
|
case out.gammaControl == nil:
|
||||||
|
continue
|
||||||
|
case !m.outputStillValid(out):
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ramp := GenerateGammaRamp(out.rampSize, temp, gamma)
|
ramp := GenerateGammaRamp(out.rampSize, temp, gamma)
|
||||||
@@ -732,18 +847,16 @@ func (m *Manager) applyGamma(temp int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if err := m.setGammaBytes(j.out, j.data); err != nil {
|
err := m.setGammaBytes(j.out, j.data)
|
||||||
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
|
if err == nil {
|
||||||
j.out.failed = true
|
continue
|
||||||
j.out.rampSize = 0
|
}
|
||||||
outID := j.out.id
|
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
|
||||||
time.AfterFunc(300*time.Millisecond, func() {
|
j.out.failed = true
|
||||||
m.post(func() {
|
j.out.rampSize = 0
|
||||||
if out, ok := m.outputs.Load(outID); ok && out.failed {
|
if isConnectionDeadErr(err) {
|
||||||
m.recreateOutputControl(out)
|
m.markConnectionDead(err)
|
||||||
}
|
return
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,6 +865,14 @@ func (m *Manager) applyGamma(temp int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
||||||
|
if out.gammaControl == nil {
|
||||||
|
return fmt.Errorf("no gamma control")
|
||||||
|
}
|
||||||
|
ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
|
||||||
|
if !ok || ctrl == nil || ctrl.IsZombie() {
|
||||||
|
return fmt.Errorf("gamma control invalid")
|
||||||
|
}
|
||||||
|
|
||||||
fd, err := MemfdCreate("gamma-ramp", 0)
|
fd, err := MemfdCreate("gamma-ramp", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -774,7 +895,6 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
|||||||
}
|
}
|
||||||
syscall.Seek(fd, 0, 0)
|
syscall.Seek(fd, 0, 0)
|
||||||
|
|
||||||
ctrl := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
|
|
||||||
return ctrl.SetGamma(fd)
|
return ctrl.SetGamma(fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,10 +1002,10 @@ func (m *Manager) dbusMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
||||||
if sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
|
switch {
|
||||||
|
case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep":
|
||||||
return
|
return
|
||||||
}
|
case len(sig.Body) == 0:
|
||||||
if len(sig.Body) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
preparing, ok := sig.Body[0].(bool)
|
preparing, ok := sig.Body[0].(bool)
|
||||||
@@ -899,27 +1019,34 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
time.AfterFunc(500*time.Millisecond, func() {
|
time.AfterFunc(500*time.Millisecond, func() {
|
||||||
m.post(func() {
|
m.post(m.handleResume)
|
||||||
m.configMutex.RLock()
|
|
||||||
stillEnabled := m.config.Enabled
|
|
||||||
m.configMutex.RUnlock()
|
|
||||||
if !stillEnabled || !m.controlsInitialized {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.outputs.Range(func(_ uint32, out *outputState) bool {
|
|
||||||
if out.gammaControl != nil {
|
|
||||||
out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
|
|
||||||
out.gammaControl = nil
|
|
||||||
}
|
|
||||||
out.retryCount = 0
|
|
||||||
out.failed = false
|
|
||||||
m.recreateOutputControl(out)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleResume() {
|
||||||
|
m.configMutex.RLock()
|
||||||
|
stillEnabled := m.config.Enabled
|
||||||
|
m.configMutex.RUnlock()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !stillEnabled:
|
||||||
|
return
|
||||||
|
case !m.controlsInitialized:
|
||||||
|
return
|
||||||
|
case m.connectionDead.Load():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma
|
||||||
|
// ramp to DRM on resume; gamma_control objects stay valid. We just need
|
||||||
|
// to force a resend so the schedule catches up with the current time of
|
||||||
|
// day — the original #1235 regression was caused by lastAppliedTemp
|
||||||
|
// matching and the send being skipped.
|
||||||
|
m.recalcSchedule(time.Now())
|
||||||
|
m.lastAppliedTemp = 0
|
||||||
|
m.applyCurrentTemp("resume")
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) triggerUpdate() {
|
func (m *Manager) triggerUpdate() {
|
||||||
select {
|
select {
|
||||||
case m.updateTrigger <- struct{}{}:
|
case m.updateTrigger <- struct{}{}:
|
||||||
@@ -1058,7 +1185,10 @@ func (m *Manager) SetEnabled(enabled bool) {
|
|||||||
case enabled && !m.controlsInitialized:
|
case enabled && !m.controlsInitialized:
|
||||||
m.post(func() {
|
m.post(func() {
|
||||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||||
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
|
m.availOutputsMu.RLock()
|
||||||
|
outs := slices.Clone(m.availableOutputs)
|
||||||
|
m.availOutputsMu.RUnlock()
|
||||||
|
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
|
||||||
log.Errorf("gamma: failed to create controls: %v", err)
|
log.Errorf("gamma: failed to create controls: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package wayland
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
@@ -71,9 +72,11 @@ type Manager struct {
|
|||||||
registry *wlclient.Registry
|
registry *wlclient.Registry
|
||||||
gammaControl any
|
gammaControl any
|
||||||
availableOutputs []*wlclient.Output
|
availableOutputs []*wlclient.Output
|
||||||
|
availOutputsMu sync.RWMutex
|
||||||
outputRegNames syncmap.Map[uint32, uint32]
|
outputRegNames syncmap.Map[uint32, uint32]
|
||||||
outputs syncmap.Map[uint32, *outputState]
|
outputs syncmap.Map[uint32, *outputState]
|
||||||
controlsInitialized bool
|
controlsInitialized bool
|
||||||
|
connectionDead atomic.Bool
|
||||||
|
|
||||||
cmdq chan cmd
|
cmdq chan cmd
|
||||||
alive bool
|
alive bool
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
if isGit {
|
if isGit {
|
||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell-bin"
|
return "dms-shell"
|
||||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||||
if isGit {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
|
|||||||
+16
@@ -502,6 +502,9 @@ Notepad/scratchpad modal control for quick note-taking.
|
|||||||
- `open` - Show notepad modal
|
- `open` - Show notepad modal
|
||||||
- `close` - Hide notepad modal
|
- `close` - Hide notepad modal
|
||||||
- `toggle` - Toggle notepad modal visibility
|
- `toggle` - Toggle notepad modal visibility
|
||||||
|
- `expand` - Expand the active notepad width and open it if hidden
|
||||||
|
- `collapse` - Collapse the active notepad width without changing visibility
|
||||||
|
- `toggleExpand` - Toggle the active notepad width between collapsed and expanded
|
||||||
|
|
||||||
### Target: `dash`
|
### Target: `dash`
|
||||||
Dashboard popup control with tab selection for overview, media, and weather information.
|
Dashboard popup control with tab selection for overview, media, and weather information.
|
||||||
@@ -610,6 +613,15 @@ dms ipc call powermenu toggle
|
|||||||
# Open notepad
|
# Open notepad
|
||||||
dms ipc call notepad toggle
|
dms ipc call notepad toggle
|
||||||
|
|
||||||
|
# Open the active notepad expanded
|
||||||
|
dms ipc call notepad expand
|
||||||
|
|
||||||
|
# Collapse the active notepad width
|
||||||
|
dms ipc call notepad collapse
|
||||||
|
|
||||||
|
# Toggle the active notepad width
|
||||||
|
dms ipc call notepad toggleExpand
|
||||||
|
|
||||||
# Show dashboard with specific tabs
|
# Show dashboard with specific tabs
|
||||||
dms ipc call dash open overview
|
dms ipc call dash open overview
|
||||||
dms ipc call dash toggle media
|
dms ipc call dash toggle media
|
||||||
@@ -647,6 +659,8 @@ binds {
|
|||||||
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
|
Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
|
Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; }
|
||||||
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
|
Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
|
Mod+Shift+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "expand"; }
|
||||||
|
Mod+Ctrl+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggleExpand"; }
|
||||||
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
|
Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; }
|
||||||
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
|
XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; }
|
||||||
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
|
XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; }
|
||||||
@@ -658,6 +672,8 @@ binds {
|
|||||||
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
|
bind = SUPER, Space, exec, qs -c dms ipc call spotlight toggle
|
||||||
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
|
bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle
|
||||||
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
|
bind = SUPER, P, exec, qs -c dms ipc call notepad toggle
|
||||||
|
bind = SUPER SHIFT, P, exec, qs -c dms ipc call notepad expand
|
||||||
|
bind = SUPER CTRL, P, exec, qs -c dms ipc call notepad toggleExpand
|
||||||
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
|
bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle
|
||||||
bind = SUPER, slash, exec, qs -c dms ipc call hypr toggleBinds
|
bind = SUPER, slash, exec, qs -c dms ipc call hypr toggleBinds
|
||||||
bind = SUPER, Tab, exec, qs -c dms ipc call hypr toggleOverview
|
bind = SUPER, Tab, exec, qs -c dms ipc call hypr toggleOverview
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
|
{ id: "spawn dms ipc call notepad toggle", label: "Notepad: Toggle" },
|
||||||
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
|
{ id: "spawn dms ipc call notepad open", label: "Notepad: Open" },
|
||||||
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
|
{ id: "spawn dms ipc call notepad close", label: "Notepad: Close" },
|
||||||
|
{ id: "spawn dms ipc call notepad expand", label: "Notepad: Expand" },
|
||||||
|
{ id: "spawn dms ipc call notepad collapse", label: "Notepad: Collapse" },
|
||||||
|
{ id: "spawn dms ipc call notepad toggleExpand", label: "Notepad: Toggle Expand" },
|
||||||
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
|
{ id: "spawn dms ipc call dash toggle \"\"", label: "Dashboard: Toggle" },
|
||||||
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
|
{ id: "spawn dms ipc call dash open overview", label: "Dashboard: Overview" },
|
||||||
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
|
{ id: "spawn dms ipc call dash open media", label: "Dashboard: Media" },
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ Singleton {
|
|||||||
|
|
||||||
property string vpnLastConnected: ""
|
property string vpnLastConnected: ""
|
||||||
|
|
||||||
|
property string lastPlayerIdentity: ""
|
||||||
|
|
||||||
property var deviceMaxVolumes: ({})
|
property var deviceMaxVolumes: ({})
|
||||||
property var hiddenOutputDeviceNames: []
|
property var hiddenOutputDeviceNames: []
|
||||||
property var hiddenInputDeviceNames: []
|
property var hiddenInputDeviceNames: []
|
||||||
|
|||||||
@@ -186,6 +186,14 @@ Singleton {
|
|||||||
onPopoutElevationEnabledChanged: saveSettings()
|
onPopoutElevationEnabledChanged: saveSettings()
|
||||||
property bool barElevationEnabled: true
|
property bool barElevationEnabled: true
|
||||||
onBarElevationEnabledChanged: saveSettings()
|
onBarElevationEnabledChanged: saveSettings()
|
||||||
|
property bool blurEnabled: false
|
||||||
|
onBlurEnabledChanged: saveSettings()
|
||||||
|
property string blurBorderColor: "outline"
|
||||||
|
onBlurBorderColorChanged: saveSettings()
|
||||||
|
property string blurBorderCustomColor: "#ffffff"
|
||||||
|
onBlurBorderCustomColorChanged: saveSettings()
|
||||||
|
property real blurBorderOpacity: 1.0
|
||||||
|
onBlurBorderOpacityChanged: saveSettings()
|
||||||
property string wallpaperFillMode: "Fill"
|
property string wallpaperFillMode: "Fill"
|
||||||
property bool blurredWallpaperLayer: false
|
property bool blurredWallpaperLayer: false
|
||||||
property bool blurWallpaperOnOverview: false
|
property bool blurWallpaperOnOverview: false
|
||||||
@@ -293,6 +301,7 @@ Singleton {
|
|||||||
property var workspaceNameIcons: ({})
|
property var workspaceNameIcons: ({})
|
||||||
property bool waveProgressEnabled: true
|
property bool waveProgressEnabled: true
|
||||||
property bool scrollTitleEnabled: true
|
property bool scrollTitleEnabled: true
|
||||||
|
property bool mediaAdaptiveWidthEnabled: true
|
||||||
property bool audioVisualizerEnabled: true
|
property bool audioVisualizerEnabled: true
|
||||||
property string audioScrollMode: "volume"
|
property string audioScrollMode: "volume"
|
||||||
property int audioWheelScrollAmount: 5
|
property int audioWheelScrollAmount: 5
|
||||||
@@ -350,6 +359,8 @@ Singleton {
|
|||||||
property string dankLauncherV2BorderColor: "primary"
|
property string dankLauncherV2BorderColor: "primary"
|
||||||
property bool dankLauncherV2ShowFooter: true
|
property bool dankLauncherV2ShowFooter: true
|
||||||
property bool dankLauncherV2UnloadOnClose: false
|
property bool dankLauncherV2UnloadOnClose: false
|
||||||
|
property bool dankLauncherV2IncludeFilesInAll: false
|
||||||
|
property bool dankLauncherV2IncludeFoldersInAll: false
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
@@ -426,17 +437,20 @@ Singleton {
|
|||||||
property bool soundNewNotification: true
|
property bool soundNewNotification: true
|
||||||
property bool soundVolumeChanged: true
|
property bool soundVolumeChanged: true
|
||||||
property bool soundPluggedIn: true
|
property bool soundPluggedIn: true
|
||||||
|
property bool soundLogin: false
|
||||||
|
|
||||||
property int acMonitorTimeout: 0
|
property int acMonitorTimeout: 0
|
||||||
property int acLockTimeout: 0
|
property int acLockTimeout: 0
|
||||||
property int acSuspendTimeout: 0
|
property int acSuspendTimeout: 0
|
||||||
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int acSuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
property string acProfileName: ""
|
property string acProfileName: ""
|
||||||
|
property int acPostLockMonitorTimeout: 0
|
||||||
property int batteryMonitorTimeout: 0
|
property int batteryMonitorTimeout: 0
|
||||||
property int batteryLockTimeout: 0
|
property int batteryLockTimeout: 0
|
||||||
property int batterySuspendTimeout: 0
|
property int batterySuspendTimeout: 0
|
||||||
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
property int batterySuspendBehavior: SettingsData.SuspendBehavior.Suspend
|
||||||
property string batteryProfileName: ""
|
property string batteryProfileName: ""
|
||||||
|
property int batteryPostLockMonitorTimeout: 0
|
||||||
property int batteryChargeLimit: 100
|
property int batteryChargeLimit: 100
|
||||||
property bool lockBeforeSuspend: false
|
property bool lockBeforeSuspend: false
|
||||||
property bool loginctlLockIntegration: true
|
property bool loginctlLockIntegration: true
|
||||||
@@ -546,24 +560,24 @@ Singleton {
|
|||||||
|
|
||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
readonly property bool fprintdAvailable: Processes.fprintdAvailable
|
||||||
property bool lockFingerprintCanEnable: false
|
readonly property bool lockFingerprintCanEnable: Processes.lockFingerprintCanEnable
|
||||||
property bool lockFingerprintReady: false
|
readonly property bool lockFingerprintReady: Processes.lockFingerprintReady
|
||||||
property string lockFingerprintReason: "probe_failed"
|
readonly property string lockFingerprintReason: Processes.lockFingerprintReason
|
||||||
property bool greeterFingerprintCanEnable: false
|
readonly property bool greeterFingerprintCanEnable: Processes.greeterFingerprintCanEnable
|
||||||
property bool greeterFingerprintReady: false
|
readonly property bool greeterFingerprintReady: Processes.greeterFingerprintReady
|
||||||
property string greeterFingerprintReason: "probe_failed"
|
readonly property string greeterFingerprintReason: Processes.greeterFingerprintReason
|
||||||
property string greeterFingerprintSource: "none"
|
readonly property string greeterFingerprintSource: Processes.greeterFingerprintSource
|
||||||
property bool enableU2f: false
|
property bool enableU2f: false
|
||||||
property string u2fMode: "or"
|
property string u2fMode: "or"
|
||||||
property bool u2fAvailable: false
|
readonly property bool u2fAvailable: Processes.u2fAvailable
|
||||||
property bool lockU2fCanEnable: false
|
readonly property bool lockU2fCanEnable: Processes.lockU2fCanEnable
|
||||||
property bool lockU2fReady: false
|
readonly property bool lockU2fReady: Processes.lockU2fReady
|
||||||
property string lockU2fReason: "probe_failed"
|
readonly property string lockU2fReason: Processes.lockU2fReason
|
||||||
property bool greeterU2fCanEnable: false
|
readonly property bool greeterU2fCanEnable: Processes.greeterU2fCanEnable
|
||||||
property bool greeterU2fReady: false
|
readonly property bool greeterU2fReady: Processes.greeterU2fReady
|
||||||
property string greeterU2fReason: "probe_failed"
|
readonly property string greeterU2fReason: Processes.greeterU2fReason
|
||||||
property string greeterU2fSource: "none"
|
readonly property string greeterU2fSource: Processes.greeterU2fSource
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
@@ -1053,7 +1067,6 @@ Singleton {
|
|||||||
function refreshAuthAvailability() {
|
function refreshAuthAvailability() {
|
||||||
if (isGreeterMode)
|
if (isGreeterMode)
|
||||||
return;
|
return;
|
||||||
Processes.settingsRoot = root;
|
|
||||||
Processes.detectAuthCapabilities();
|
Processes.detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,16 +25,10 @@ Singleton {
|
|||||||
|
|
||||||
property string fingerprintProbeOutput: ""
|
property string fingerprintProbeOutput: ""
|
||||||
property int fingerprintProbeExitCode: 0
|
property int fingerprintProbeExitCode: 0
|
||||||
property bool fingerprintProbeStreamFinished: false
|
property bool fingerprintProbeFinalized: false
|
||||||
property bool fingerprintProbeExited: false
|
|
||||||
property string fingerprintProbeState: "probe_failed"
|
|
||||||
|
|
||||||
property string pamSupportProbeOutput: ""
|
property string pamProbeOutput: ""
|
||||||
property bool pamSupportProbeStreamFinished: false
|
property bool pamProbeFinalized: false
|
||||||
property bool pamSupportProbeExited: false
|
|
||||||
property int pamSupportProbeExitCode: 0
|
|
||||||
property bool pamFprintSupportDetected: false
|
|
||||||
property bool pamU2fSupportDetected: false
|
|
||||||
|
|
||||||
readonly property string homeDir: Quickshell.env("HOME") || ""
|
readonly property string homeDir: Quickshell.env("HOME") || ""
|
||||||
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
||||||
@@ -54,40 +48,189 @@ Singleton {
|
|||||||
|
|
||||||
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||||
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||||
property bool authApplyRunning: false
|
|
||||||
property bool authApplyQueued: false
|
|
||||||
property bool authApplyRerunRequested: false
|
|
||||||
property bool authApplyTerminalFallbackFromPrecheck: false
|
|
||||||
property string authApplyStdout: ""
|
|
||||||
property string authApplyStderr: ""
|
|
||||||
property string authApplySudoProbeStderr: ""
|
|
||||||
property string authApplyTerminalFallbackStderr: ""
|
|
||||||
|
|
||||||
function detectQtTools() {
|
// --- Derived auth probe state ---
|
||||||
qtToolsDetectionProcess.running = true;
|
|
||||||
|
readonly property bool pamFprintSupportDetected: pamProbeFinalized && pamProbeOutput.includes("pam_fprintd.so:true")
|
||||||
|
readonly property bool pamU2fSupportDetected: pamProbeFinalized && pamProbeOutput.includes("pam_u2f.so:true")
|
||||||
|
|
||||||
|
readonly property string fingerprintProbeState: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
if (!fingerprintProbeFinalized)
|
||||||
|
return "probe_failed";
|
||||||
|
return parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput, pamFprintSupportDetected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectAuthCapabilities() {
|
// --- Lock fingerprint capabilities ---
|
||||||
if (!settingsRoot)
|
|
||||||
return;
|
|
||||||
|
|
||||||
|
readonly property bool lockFingerprintCanEnable: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable;
|
||||||
|
switch (fingerprintProbeState) {
|
||||||
|
case "ready":
|
||||||
|
case "missing_enrollment":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool lockFingerprintReady: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable;
|
||||||
|
return fingerprintProbeState === "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string lockFingerprintReason: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
return fingerprintProbeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Greeter fingerprint capabilities ---
|
||||||
|
|
||||||
|
readonly property bool greeterFingerprintCanEnable: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable;
|
||||||
|
if (greeterPamHasFprint)
|
||||||
|
return fingerprintProbeState !== "missing_reader";
|
||||||
|
switch (fingerprintProbeState) {
|
||||||
|
case "ready":
|
||||||
|
case "missing_enrollment":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool greeterFingerprintReady: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable;
|
||||||
|
return fingerprintProbeState === "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string greeterFingerprintReason: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
if (greeterPamHasFprint) {
|
||||||
|
switch (fingerprintProbeState) {
|
||||||
|
case "ready":
|
||||||
|
return "configured_externally";
|
||||||
|
case "missing_enrollment":
|
||||||
|
return "missing_enrollment";
|
||||||
|
case "missing_reader":
|
||||||
|
return "missing_reader";
|
||||||
|
default:
|
||||||
|
return "probe_failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fingerprintProbeState;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string greeterFingerprintSource: {
|
||||||
|
if (forcedFprintAvailable !== null)
|
||||||
|
return forcedFprintAvailable ? "dms" : "none";
|
||||||
|
if (greeterPamHasFprint)
|
||||||
|
return "pam";
|
||||||
|
switch (fingerprintProbeState) {
|
||||||
|
case "ready":
|
||||||
|
case "missing_enrollment":
|
||||||
|
return "dms";
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lock U2F capabilities ---
|
||||||
|
|
||||||
|
readonly property bool lockU2fReady: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable;
|
||||||
|
return lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool lockU2fCanEnable: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable;
|
||||||
|
return lockU2fReady || pamU2fSupportDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string lockU2fReason: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable ? "ready" : "probe_failed";
|
||||||
|
if (lockU2fReady)
|
||||||
|
return "ready";
|
||||||
|
if (lockU2fCanEnable)
|
||||||
|
return "missing_key_registration";
|
||||||
|
return "missing_pam_support";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Greeter U2F capabilities ---
|
||||||
|
|
||||||
|
readonly property bool greeterU2fReady: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable;
|
||||||
|
if (greeterPamHasU2f)
|
||||||
|
return true;
|
||||||
|
return homeU2fKeysDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool greeterU2fCanEnable: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable;
|
||||||
|
if (greeterPamHasU2f)
|
||||||
|
return true;
|
||||||
|
return greeterU2fReady || pamU2fSupportDetected;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string greeterU2fReason: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable ? "ready" : "probe_failed";
|
||||||
|
if (greeterPamHasU2f)
|
||||||
|
return "configured_externally";
|
||||||
|
if (greeterU2fReady)
|
||||||
|
return "ready";
|
||||||
|
if (greeterU2fCanEnable)
|
||||||
|
return "missing_key_registration";
|
||||||
|
return "missing_pam_support";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string greeterU2fSource: {
|
||||||
|
if (forcedU2fAvailable !== null)
|
||||||
|
return forcedU2fAvailable ? "dms" : "none";
|
||||||
|
if (greeterPamHasU2f)
|
||||||
|
return "pam";
|
||||||
|
if (greeterU2fCanEnable)
|
||||||
|
return "dms";
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Aggregates ---
|
||||||
|
|
||||||
|
readonly property bool fprintdAvailable: lockFingerprintReady || greeterFingerprintReady
|
||||||
|
readonly property bool u2fAvailable: lockU2fReady || greeterU2fReady
|
||||||
|
|
||||||
|
// --- Auth detection ---
|
||||||
|
|
||||||
|
readonly property var _fprintProbeCommand: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
||||||
|
readonly property var _pamProbeCommand: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
||||||
|
|
||||||
|
function detectAuthCapabilities() {
|
||||||
if (forcedFprintAvailable === null) {
|
if (forcedFprintAvailable === null) {
|
||||||
fingerprintProbeOutput = "";
|
fingerprintProbeFinalized = false;
|
||||||
fingerprintProbeStreamFinished = false;
|
Proc.runCommand("fprint-probe", _fprintProbeCommand, (output, exitCode) => {
|
||||||
fingerprintProbeExited = false;
|
fingerprintProbeOutput = output || "";
|
||||||
fingerprintProbeProcess.running = true;
|
fingerprintProbeExitCode = exitCode;
|
||||||
} else {
|
fingerprintProbeFinalized = true;
|
||||||
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pamFprintSupportDetected = false;
|
pamProbeFinalized = false;
|
||||||
pamU2fSupportDetected = false;
|
Proc.runCommand("pam-probe", _pamProbeCommand, (output, _exitCode) => {
|
||||||
pamSupportProbeOutput = "";
|
pamProbeOutput = output || "";
|
||||||
pamSupportProbeStreamFinished = false;
|
pamProbeFinalized = true;
|
||||||
pamSupportProbeExited = false;
|
}, 0);
|
||||||
pamSupportDetectionProcess.running = true;
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
@@ -98,9 +241,16 @@ Singleton {
|
|||||||
detectAuthCapabilities();
|
detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
// --- Auth apply pipeline ---
|
||||||
pluginSettingsCheckProcess.running = true;
|
|
||||||
}
|
property bool authApplyRunning: false
|
||||||
|
property bool authApplyQueued: false
|
||||||
|
property bool authApplyRerunRequested: false
|
||||||
|
property bool authApplyTerminalFallbackFromPrecheck: false
|
||||||
|
property string authApplyStdout: ""
|
||||||
|
property string authApplyStderr: ""
|
||||||
|
property string authApplySudoProbeStderr: ""
|
||||||
|
property string authApplyTerminalFallbackStderr: ""
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
function scheduleAuthApply() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot || settingsRoot.isGreeterMode)
|
||||||
@@ -146,6 +296,8 @@ Singleton {
|
|||||||
authApplyDebounce.restart();
|
authApplyDebounce.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- PAM parsing helpers ---
|
||||||
|
|
||||||
function stripPamComment(line) {
|
function stripPamComment(line) {
|
||||||
if (!line)
|
if (!line)
|
||||||
return "";
|
return "";
|
||||||
@@ -189,15 +341,7 @@ Singleton {
|
|||||||
function greeterPamStackHasModule(moduleName) {
|
function greeterPamStackHasModule(moduleName) {
|
||||||
if (pamModuleEnabled(greetdPamText, moduleName))
|
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||||
return true;
|
return true;
|
||||||
const includedPamStacks = [
|
const includedPamStacks = [["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText]];
|
||||||
["system-auth", systemAuthPamText],
|
|
||||||
["common-auth", commonAuthPamText],
|
|
||||||
["password-auth", passwordAuthPamText],
|
|
||||||
["system-login", systemLoginPamText],
|
|
||||||
["system-local-login", systemLocalLoginPamText],
|
|
||||||
["common-auth-pc", commonAuthPcPamText],
|
|
||||||
["login", loginPamText]
|
|
||||||
];
|
|
||||||
for (let i = 0; i < includedPamStacks.length; i++) {
|
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||||
const stack = includedPamStacks[i];
|
const stack = includedPamStacks[i];
|
||||||
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||||
@@ -206,6 +350,8 @@ Singleton {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Fingerprint probe output parsing ---
|
||||||
|
|
||||||
function hasEnrolledFingerprintOutput(output) {
|
function hasEnrolledFingerprintOutput(output) {
|
||||||
const lower = (output || "").toLowerCase();
|
const lower = (output || "").toLowerCase();
|
||||||
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
||||||
@@ -223,21 +369,15 @@ Singleton {
|
|||||||
|
|
||||||
function hasMissingFingerprintEnrollmentOutput(output) {
|
function hasMissingFingerprintEnrollmentOutput(output) {
|
||||||
const lower = (output || "").toLowerCase();
|
const lower = (output || "").toLowerCase();
|
||||||
return lower.includes("no fingers enrolled")
|
return lower.includes("no fingers enrolled") || lower.includes("no fingerprints enrolled") || lower.includes("no prints enrolled");
|
||||||
|| lower.includes("no fingerprints enrolled")
|
|
||||||
|| lower.includes("no prints enrolled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMissingFingerprintReaderOutput(output) {
|
function hasMissingFingerprintReaderOutput(output) {
|
||||||
const lower = (output || "").toLowerCase();
|
const lower = (output || "").toLowerCase();
|
||||||
return lower.includes("no devices available")
|
return lower.includes("no devices available") || lower.includes("no device available") || lower.includes("no devices found") || lower.includes("list_devices failed") || lower.includes("no device");
|
||||||
|| lower.includes("no device available")
|
|
||||||
|| lower.includes("no devices found")
|
|
||||||
|| lower.includes("list_devices failed")
|
|
||||||
|| lower.includes("no device");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFingerprintProbe(exitCode, output) {
|
function parseFingerprintProbe(exitCode, output, pamFprintDetected) {
|
||||||
if (hasEnrolledFingerprintOutput(output))
|
if (hasEnrolledFingerprintOutput(output))
|
||||||
return "ready";
|
return "ready";
|
||||||
if (hasMissingFingerprintEnrollmentOutput(output))
|
if (hasMissingFingerprintEnrollmentOutput(output))
|
||||||
@@ -248,164 +388,17 @@ Singleton {
|
|||||||
return "missing_enrollment";
|
return "missing_enrollment";
|
||||||
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
||||||
return "probe_failed";
|
return "probe_failed";
|
||||||
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
|
return pamFprintDetected ? "probe_failed" : "missing_pam_support";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLockFingerprintCapability(canEnable, ready, reason) {
|
// --- Qt tools detection ---
|
||||||
settingsRoot.lockFingerprintCanEnable = canEnable;
|
|
||||||
settingsRoot.lockFingerprintReady = ready;
|
function detectQtTools() {
|
||||||
settingsRoot.lockFingerprintReason = reason;
|
qtToolsDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLockU2fCapability(canEnable, ready, reason) {
|
function checkPluginSettings() {
|
||||||
settingsRoot.lockU2fCanEnable = canEnable;
|
pluginSettingsCheckProcess.running = true;
|
||||||
settingsRoot.lockU2fReady = ready;
|
|
||||||
settingsRoot.lockU2fReason = reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
|
|
||||||
settingsRoot.greeterFingerprintCanEnable = canEnable;
|
|
||||||
settingsRoot.greeterFingerprintReady = ready;
|
|
||||||
settingsRoot.greeterFingerprintReason = reason;
|
|
||||||
settingsRoot.greeterFingerprintSource = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGreeterU2fCapability(canEnable, ready, reason, source) {
|
|
||||||
settingsRoot.greeterU2fCanEnable = canEnable;
|
|
||||||
settingsRoot.greeterU2fReady = ready;
|
|
||||||
settingsRoot.greeterU2fReason = reason;
|
|
||||||
settingsRoot.greeterU2fSource = source;
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeFingerprintCapabilities() {
|
|
||||||
if (forcedFprintAvailable !== null) {
|
|
||||||
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
|
|
||||||
const source = forcedFprintAvailable ? "dms" : "none";
|
|
||||||
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
|
|
||||||
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = fingerprintProbeState;
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setLockFingerprintCapability(true, true, "ready");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setLockFingerprintCapability(true, false, "missing_enrollment");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setLockFingerprintCapability(false, false, "missing_reader");
|
|
||||||
break;
|
|
||||||
case "missing_pam_support":
|
|
||||||
setLockFingerprintCapability(false, false, "missing_pam_support");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setLockFingerprintCapability(false, false, "probe_failed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (greeterPamHasFprint) {
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case "ready":
|
|
||||||
setGreeterFingerprintCapability(true, true, "ready", "dms");
|
|
||||||
break;
|
|
||||||
case "missing_enrollment":
|
|
||||||
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
|
|
||||||
break;
|
|
||||||
case "missing_reader":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
|
|
||||||
break;
|
|
||||||
case "missing_pam_support":
|
|
||||||
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeU2fCapabilities() {
|
|
||||||
if (forcedU2fAvailable !== null) {
|
|
||||||
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
|
|
||||||
const source = forcedU2fAvailable ? "dms" : "none";
|
|
||||||
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
|
|
||||||
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
|
||||||
const lockCanEnable = lockReady || pamU2fSupportDetected;
|
|
||||||
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
|
|
||||||
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
|
|
||||||
|
|
||||||
if (greeterPamHasU2f) {
|
|
||||||
setGreeterU2fCapability(true, true, "configured_externally", "pam");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const greeterReady = homeU2fKeysDetected;
|
|
||||||
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
|
|
||||||
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
|
|
||||||
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeAuthCapabilities() {
|
|
||||||
if (!settingsRoot)
|
|
||||||
return;
|
|
||||||
recomputeFingerprintCapabilities();
|
|
||||||
recomputeU2fCapabilities();
|
|
||||||
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
|
|
||||||
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizeFingerprintProbe() {
|
|
||||||
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
|
|
||||||
return;
|
|
||||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizePamSupportProbe() {
|
|
||||||
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
|
|
||||||
return;
|
|
||||||
|
|
||||||
pamFprintSupportDetected = false;
|
|
||||||
pamU2fSupportDetected = false;
|
|
||||||
|
|
||||||
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length !== 2)
|
|
||||||
continue;
|
|
||||||
if (parts[0] === "pam_fprintd.so")
|
|
||||||
pamFprintSupportDetected = parts[1] === "true";
|
|
||||||
else if (parts[0] === "pam_u2f.so")
|
|
||||||
pamU2fSupportDetected = parts[1] === "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
|
|
||||||
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property var qtToolsDetectionProcess: Process {
|
property var qtToolsDetectionProcess: Process {
|
||||||
@@ -433,44 +426,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fingerprintProbeProcess: Process {
|
|
||||||
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.fingerprintProbeOutput = text || "";
|
|
||||||
root.fingerprintProbeStreamFinished = true;
|
|
||||||
root.finalizeFingerprintProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
root.fingerprintProbeExitCode = exitCode;
|
|
||||||
root.fingerprintProbeExited = true;
|
|
||||||
root.finalizeFingerprintProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var pamSupportDetectionProcess: Process {
|
|
||||||
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
root.pamSupportProbeOutput = text || "";
|
|
||||||
root.pamSupportProbeStreamFinished = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
|
||||||
root.pamSupportProbeExitCode = exitCode;
|
|
||||||
root.pamSupportProbeExited = true;
|
|
||||||
root.finalizePamSupportProbe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: authApplyDebounce
|
id: authApplyDebounce
|
||||||
interval: 300
|
interval: 300
|
||||||
@@ -544,9 +499,7 @@ Singleton {
|
|||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
const message = root.authApplyTerminalFallbackFromPrecheck
|
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
|
||||||
? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.")
|
|
||||||
: I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
|
|
||||||
ToastService.showInfo(message, "", "", "auth-sync");
|
ToastService.showInfo(message, "", "", "auth-sync");
|
||||||
} else {
|
} else {
|
||||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
||||||
@@ -560,140 +513,80 @@ Singleton {
|
|||||||
id: greetdPamWatcher
|
id: greetdPamWatcher
|
||||||
path: "/etc/pam.d/greetd"
|
path: "/etc/pam.d/greetd"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.greetdPamText = text()
|
||||||
root.greetdPamText = text();
|
onLoadFailed: root.greetdPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.greetdPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: systemAuthPamWatcher
|
id: systemAuthPamWatcher
|
||||||
path: "/etc/pam.d/system-auth"
|
path: "/etc/pam.d/system-auth"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.systemAuthPamText = text()
|
||||||
root.systemAuthPamText = text();
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: commonAuthPamWatcher
|
id: commonAuthPamWatcher
|
||||||
path: "/etc/pam.d/common-auth"
|
path: "/etc/pam.d/common-auth"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.commonAuthPamText = text()
|
||||||
root.commonAuthPamText = text();
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.commonAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: passwordAuthPamWatcher
|
id: passwordAuthPamWatcher
|
||||||
path: "/etc/pam.d/password-auth"
|
path: "/etc/pam.d/password-auth"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
root.passwordAuthPamText = text();
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.passwordAuthPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: systemLoginPamWatcher
|
id: systemLoginPamWatcher
|
||||||
path: "/etc/pam.d/system-login"
|
path: "/etc/pam.d/system-login"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.systemLoginPamText = text()
|
||||||
root.systemLoginPamText = text();
|
onLoadFailed: root.systemLoginPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemLoginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: systemLocalLoginPamWatcher
|
id: systemLocalLoginPamWatcher
|
||||||
path: "/etc/pam.d/system-local-login"
|
path: "/etc/pam.d/system-local-login"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.systemLocalLoginPamText = text()
|
||||||
root.systemLocalLoginPamText = text();
|
onLoadFailed: root.systemLocalLoginPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.systemLocalLoginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: commonAuthPcPamWatcher
|
id: commonAuthPcPamWatcher
|
||||||
path: "/etc/pam.d/common-auth-pc"
|
path: "/etc/pam.d/common-auth-pc"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.commonAuthPcPamText = text()
|
||||||
root.commonAuthPcPamText = text();
|
onLoadFailed: root.commonAuthPcPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.commonAuthPcPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: loginPamWatcher
|
id: loginPamWatcher
|
||||||
path: "/etc/pam.d/login"
|
path: "/etc/pam.d/login"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.loginPamText = text()
|
||||||
root.loginPamText = text();
|
onLoadFailed: root.loginPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.loginPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dankshellU2fPamWatcher
|
id: dankshellU2fPamWatcher
|
||||||
path: "/etc/pam.d/dankshell-u2f"
|
path: "/etc/pam.d/dankshell-u2f"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.dankshellU2fPamText = text()
|
||||||
root.dankshellU2fPamText = text();
|
onLoadFailed: root.dankshellU2fPamText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.dankshellU2fPamText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: u2fKeysWatcher
|
id: u2fKeysWatcher
|
||||||
path: root.u2fKeysPath
|
path: root.u2fKeysPath
|
||||||
printErrors: false
|
printErrors: false
|
||||||
onLoaded: {
|
onLoaded: root.u2fKeysText = text()
|
||||||
root.u2fKeysText = text();
|
onLoadFailed: root.u2fKeysText = ""
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.u2fKeysText = "";
|
|
||||||
root.recomputeAuthCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
property var pluginSettingsCheckProcess: Process {
|
property var pluginSettingsCheckProcess: Process {
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ var SPEC = {
|
|||||||
|
|
||||||
vpnLastConnected: { def: "" },
|
vpnLastConnected: { def: "" },
|
||||||
|
|
||||||
|
lastPlayerIdentity: { def: "" },
|
||||||
|
|
||||||
deviceMaxVolumes: { def: {} },
|
deviceMaxVolumes: { def: {} },
|
||||||
hiddenOutputDeviceNames: { def: [] },
|
hiddenOutputDeviceNames: { def: [] },
|
||||||
hiddenInputDeviceNames: { def: [] },
|
hiddenInputDeviceNames: { def: [] },
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ var SPEC = {
|
|||||||
modalElevationEnabled: { def: true },
|
modalElevationEnabled: { def: true },
|
||||||
popoutElevationEnabled: { def: true },
|
popoutElevationEnabled: { def: true },
|
||||||
barElevationEnabled: { def: true },
|
barElevationEnabled: { def: true },
|
||||||
|
blurEnabled: { def: false },
|
||||||
|
blurBorderColor: { def: "outline" },
|
||||||
|
blurBorderCustomColor: { def: "#ffffff" },
|
||||||
|
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||||
wallpaperFillMode: { def: "Fill" },
|
wallpaperFillMode: { def: "Fill" },
|
||||||
blurredWallpaperLayer: { def: false },
|
blurredWallpaperLayer: { def: false },
|
||||||
blurWallpaperOnOverview: { def: false },
|
blurWallpaperOnOverview: { def: false },
|
||||||
@@ -136,6 +140,7 @@ var SPEC = {
|
|||||||
workspaceNameIcons: { def: {} },
|
workspaceNameIcons: { def: {} },
|
||||||
waveProgressEnabled: { def: true },
|
waveProgressEnabled: { def: true },
|
||||||
scrollTitleEnabled: { def: true },
|
scrollTitleEnabled: { def: true },
|
||||||
|
mediaAdaptiveWidthEnabled: { def: true },
|
||||||
audioVisualizerEnabled: { def: true },
|
audioVisualizerEnabled: { def: true },
|
||||||
audioScrollMode: { def: "volume" },
|
audioScrollMode: { def: "volume" },
|
||||||
audioWheelScrollAmount: { def: 5 },
|
audioWheelScrollAmount: { def: 5 },
|
||||||
@@ -199,6 +204,8 @@ var SPEC = {
|
|||||||
dankLauncherV2BorderColor: { def: "primary" },
|
dankLauncherV2BorderColor: { def: "primary" },
|
||||||
dankLauncherV2ShowFooter: { def: true },
|
dankLauncherV2ShowFooter: { def: true },
|
||||||
dankLauncherV2UnloadOnClose: { def: false },
|
dankLauncherV2UnloadOnClose: { def: false },
|
||||||
|
dankLauncherV2IncludeFilesInAll: { def: false },
|
||||||
|
dankLauncherV2IncludeFoldersInAll: { def: false },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
@@ -238,6 +245,7 @@ var SPEC = {
|
|||||||
|
|
||||||
soundsEnabled: { def: true },
|
soundsEnabled: { def: true },
|
||||||
useSystemSoundTheme: { def: false },
|
useSystemSoundTheme: { def: false },
|
||||||
|
soundLogin: { def: false },
|
||||||
soundNewNotification: { def: true },
|
soundNewNotification: { def: true },
|
||||||
soundVolumeChanged: { def: true },
|
soundVolumeChanged: { def: true },
|
||||||
soundPluggedIn: { def: true },
|
soundPluggedIn: { def: true },
|
||||||
@@ -247,11 +255,13 @@ var SPEC = {
|
|||||||
acSuspendTimeout: { def: 0 },
|
acSuspendTimeout: { def: 0 },
|
||||||
acSuspendBehavior: { def: 0 },
|
acSuspendBehavior: { def: 0 },
|
||||||
acProfileName: { def: "" },
|
acProfileName: { def: "" },
|
||||||
|
acPostLockMonitorTimeout: { def: 0 },
|
||||||
batteryMonitorTimeout: { def: 0 },
|
batteryMonitorTimeout: { def: 0 },
|
||||||
batteryLockTimeout: { def: 0 },
|
batteryLockTimeout: { def: 0 },
|
||||||
batterySuspendTimeout: { def: 0 },
|
batterySuspendTimeout: { def: 0 },
|
||||||
batterySuspendBehavior: { def: 0 },
|
batterySuspendBehavior: { def: 0 },
|
||||||
batteryProfileName: { def: "" },
|
batteryProfileName: { def: "" },
|
||||||
|
batteryPostLockMonitorTimeout: { def: 0 },
|
||||||
batteryChargeLimit: { def: 100 },
|
batteryChargeLimit: { def: 100 },
|
||||||
lockBeforeSuspend: { def: false },
|
lockBeforeSuspend: { def: false },
|
||||||
loginctlLockIntegration: { def: true },
|
loginctlLockIntegration: { def: true },
|
||||||
@@ -354,26 +364,10 @@ var SPEC = {
|
|||||||
lockScreenShowMediaPlayer: { def: true },
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
lockAtStartup: { def: false },
|
lockAtStartup: { def: false },
|
||||||
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
|
||||||
lockFingerprintCanEnable: { def: false, persist: false },
|
|
||||||
lockFingerprintReady: { def: false, persist: false },
|
|
||||||
lockFingerprintReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterFingerprintCanEnable: { def: false, persist: false },
|
|
||||||
greeterFingerprintReady: { def: false, persist: false },
|
|
||||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterFingerprintSource: { def: "none", persist: false },
|
|
||||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
u2fAvailable: { def: false, persist: false },
|
|
||||||
lockU2fCanEnable: { def: false, persist: false },
|
|
||||||
lockU2fReady: { def: false, persist: false },
|
|
||||||
lockU2fReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterU2fCanEnable: { def: false, persist: false },
|
|
||||||
greeterU2fReady: { def: false, persist: false },
|
|
||||||
greeterU2fReason: { def: "probe_failed", persist: false },
|
|
||||||
greeterU2fSource: { def: "none", persist: false },
|
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
|||||||
@@ -221,10 +221,22 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: loginSoundTimer
|
||||||
|
// Half a second delay before playing login sound, otherwise the sound may be cut off
|
||||||
|
// 50 is the minimum that seems to work, but 500 is safer
|
||||||
|
interval: 500
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
AudioService.playLoginSoundIfApplicable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
PolkitService.polkitAvailable;
|
PolkitService.polkitAvailable;
|
||||||
|
loginSoundTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -310,6 +310,37 @@ Item {
|
|||||||
return "NOTEPAD_TOGGLE_FAILED";
|
return "NOTEPAD_TOGGLE_FAILED";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expand(): string {
|
||||||
|
var instance = getActiveNotepadInstance();
|
||||||
|
if (instance) {
|
||||||
|
instance.expandedWidth = true;
|
||||||
|
if (!instance.isVisible)
|
||||||
|
instance.show();
|
||||||
|
return "NOTEPAD_EXPAND_SUCCESS";
|
||||||
|
}
|
||||||
|
return "NOTEPAD_EXPAND_FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapse(): string {
|
||||||
|
var instance = getActiveNotepadInstance();
|
||||||
|
if (instance) {
|
||||||
|
instance.expandedWidth = false;
|
||||||
|
if (!instance.isVisible)
|
||||||
|
instance.show();
|
||||||
|
return "NOTEPAD_COLLAPSE_SUCCESS";
|
||||||
|
}
|
||||||
|
return "NOTEPAD_COLLAPSE_FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(): string {
|
||||||
|
var instance = getActiveNotepadInstance();
|
||||||
|
if (instance) {
|
||||||
|
instance.expandedWidth = !instance.expandedWidth;
|
||||||
|
return "NOTEPAD_TOGGLE_EXPAND_SUCCESS";
|
||||||
|
}
|
||||||
|
return "NOTEPAD_TOGGLE_EXPAND_FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
target: "notepad"
|
target: "notepad"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,9 +400,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function previous(): void {
|
function previous(): void {
|
||||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
MprisController.previousOrRewind();
|
||||||
MprisController.activePlayer.previous();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function next(): void {
|
function next(): void {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No recent clipboard entries found")
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
@@ -181,7 +181,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No saved clipboard entries")
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -60,15 +60,12 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!clipboardAvailable) {
|
|
||||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open();
|
open();
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
shouldHaveFocus = true;
|
shouldHaveFocus = true;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
ClipboardService.refresh();
|
if (clipboardAvailable)
|
||||||
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
|
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
|
|||||||
@@ -50,14 +50,11 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!clipboardAvailable) {
|
|
||||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open();
|
open();
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
ClipboardService.refresh();
|
if (clipboardAvailable)
|
||||||
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
|
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
@@ -122,10 +119,10 @@ DankPopout {
|
|||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
if (!shouldBeVisible) {
|
if (!shouldBeVisible)
|
||||||
return;
|
return;
|
||||||
}
|
if (clipboardAvailable)
|
||||||
ClipboardService.refresh();
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
if (contentLoader.item?.searchField) {
|
if (contentLoader.item?.searchField) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Quickshell
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -30,7 +31,7 @@ Item {
|
|||||||
property real animationOffset: Theme.spacingL
|
property real animationOffset: Theme.spacingL
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
@@ -59,11 +60,25 @@ Item {
|
|||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
const focusedScreen = CompositorService.getFocusedScreen();
|
const focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||||
if (focusedScreen) {
|
if (focusedScreen) {
|
||||||
|
if (screenChanged)
|
||||||
|
contentWindow.visible = false;
|
||||||
contentWindow.screen = focusedScreen;
|
contentWindow.screen = focusedScreen;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow) {
|
||||||
|
if (screenChanged)
|
||||||
|
clickCatcher.visible = false;
|
||||||
clickCatcher.screen = focusedScreen;
|
clickCatcher.screen = focusedScreen;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (screenChanged) {
|
||||||
|
Qt.callLater(() => root._finishOpen());
|
||||||
|
} else {
|
||||||
|
_finishOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _finishOpen() {
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
shouldBeVisible = true;
|
shouldBeVisible = true;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow)
|
||||||
@@ -215,6 +230,16 @@ Item {
|
|||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: contentWindow
|
||||||
|
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||||
|
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||||
|
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||||
|
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
||||||
|
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
||||||
|
blurRadius: root.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: root.layerNamespace
|
WlrLayershell.namespace: root.layerNamespace
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: {
|
||||||
if (root.useOverlayLayer)
|
if (root.useOverlayLayer)
|
||||||
@@ -393,6 +418,15 @@ Item {
|
|||||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: root.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: BlurService.borderColor
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: root.shouldBeVisible
|
focus: root.shouldBeVisible
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ DankModal {
|
|||||||
|
|
||||||
modalWidth: 680
|
modalWidth: 680
|
||||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
||||||
backgroundColor: Theme.surfaceContainer
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
|
|||||||
@@ -352,7 +352,8 @@ Item {
|
|||||||
searchQuery = query;
|
searchQuery = query;
|
||||||
searchDebounce.restart();
|
searchDebounce.restart();
|
||||||
|
|
||||||
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/")) && query.length > 0) {
|
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
|
||||||
|
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
|
||||||
fileSearchDebounce.restart();
|
fileSearchDebounce.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +370,8 @@ Item {
|
|||||||
searchMode = mode;
|
searchMode = mode;
|
||||||
modeChanged(mode);
|
modeChanged(mode);
|
||||||
performSearch();
|
performSearch();
|
||||||
if (mode === "files") {
|
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
||||||
|
if (mode === "files" || filesInAll) {
|
||||||
fileSearchDebounce.restart();
|
fileSearchDebounce.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,10 +929,22 @@ Item {
|
|||||||
if (!DSearchService.dsearchAvailable)
|
if (!DSearchService.dsearchAvailable)
|
||||||
return;
|
return;
|
||||||
var fileQuery = "";
|
var fileQuery = "";
|
||||||
|
var effectiveType = fileSearchType || "all";
|
||||||
|
var includeFiles = SettingsData.dankLauncherV2IncludeFilesInAll;
|
||||||
|
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
|
||||||
|
|
||||||
if (searchQuery.startsWith("/")) {
|
if (searchQuery.startsWith("/")) {
|
||||||
fileQuery = searchQuery.substring(1).trim();
|
fileQuery = searchQuery.substring(1).trim();
|
||||||
} else if (searchMode === "files") {
|
} else if (searchMode === "files") {
|
||||||
fileQuery = searchQuery.trim();
|
fileQuery = searchQuery.trim();
|
||||||
|
} else if (searchMode === "all" && (includeFiles || includeFolders)) {
|
||||||
|
fileQuery = searchQuery.trim();
|
||||||
|
if (includeFiles && !includeFolders)
|
||||||
|
effectiveType = "file";
|
||||||
|
else if (!includeFiles && includeFolders)
|
||||||
|
effectiveType = "dir";
|
||||||
|
else
|
||||||
|
effectiveType = "all";
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -941,109 +955,129 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFileSearching = true;
|
isFileSearching = true;
|
||||||
var params = {
|
|
||||||
limit: 20,
|
|
||||||
fuzzy: true,
|
|
||||||
sort: fileSearchSort || "score",
|
|
||||||
desc: true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DSearchService.supportsTypeFilter) {
|
var splitBothTypes = searchMode === "all" && includeFiles && includeFolders && DSearchService.supportsTypeFilter;
|
||||||
params.type = (fileSearchType && fileSearchType !== "all") ? fileSearchType : "all";
|
var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType];
|
||||||
}
|
var pending = queryTypes.length;
|
||||||
if (fileSearchExt) {
|
var aggregatedItems = [];
|
||||||
params.ext = fileSearchExt;
|
|
||||||
}
|
|
||||||
if (fileSearchFolder) {
|
|
||||||
params.folder = fileSearchFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
DSearchService.search(fileQuery, params, function (response) {
|
for (var t = 0; t < queryTypes.length; t++) {
|
||||||
isFileSearching = false;
|
var queryType = queryTypes[t];
|
||||||
if (response.error)
|
var params = {
|
||||||
return;
|
limit: 20,
|
||||||
var fileItems = [];
|
fuzzy: true,
|
||||||
var hits = response.result?.hits || [];
|
sort: fileSearchSort || "score",
|
||||||
|
desc: true
|
||||||
|
};
|
||||||
|
|
||||||
for (var i = 0; i < hits.length; i++) {
|
if (DSearchService.supportsTypeFilter) {
|
||||||
var hit = hits[i];
|
params.type = (queryType && queryType !== "all") ? queryType : "all";
|
||||||
var docTypes = hit.locations?.doc_type;
|
}
|
||||||
var isDir = docTypes ? !!docTypes["dir"] : false;
|
if (fileSearchExt) {
|
||||||
fileItems.push(transformFileResult({
|
params.ext = fileSearchExt;
|
||||||
path: hit.id || "",
|
}
|
||||||
score: hit.score || 0,
|
if (fileSearchFolder) {
|
||||||
is_dir: isDir
|
params.folder = fileSearchFolder;
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSections = [];
|
DSearchService.search(fileQuery, params, function (response) {
|
||||||
var showType = fileSearchType || "all";
|
pending--;
|
||||||
|
if (!response.error) {
|
||||||
|
var hits = response.result?.hits || [];
|
||||||
|
for (var i = 0; i < hits.length; i++) {
|
||||||
|
var hit = hits[i];
|
||||||
|
var docTypes = hit.locations?.doc_type;
|
||||||
|
var isDir = docTypes ? !!docTypes["dir"] : false;
|
||||||
|
aggregatedItems.push(transformFileResult({
|
||||||
|
path: hit.id || "",
|
||||||
|
score: hit.score || 0,
|
||||||
|
is_dir: isDir
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pending > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (showType === "all" && DSearchService.supportsTypeFilter) {
|
isFileSearching = false;
|
||||||
var onlyFiles = [];
|
_applyFileSearchResults(aggregatedItems, effectiveType);
|
||||||
var onlyDirs = [];
|
|
||||||
for (var j = 0; j < fileItems.length; j++) {
|
|
||||||
if (fileItems[j].data?.is_dir)
|
|
||||||
onlyDirs.push(fileItems[j]);
|
|
||||||
else
|
|
||||||
onlyFiles.push(fileItems[j]);
|
|
||||||
}
|
|
||||||
if (onlyFiles.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "files",
|
|
||||||
title: I18n.tr("Files"),
|
|
||||||
icon: "insert_drive_file",
|
|
||||||
priority: 4,
|
|
||||||
items: onlyFiles,
|
|
||||||
collapsed: collapsedSections["files"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (onlyDirs.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "folders",
|
|
||||||
title: I18n.tr("Folders"),
|
|
||||||
icon: "folder",
|
|
||||||
priority: 4.1,
|
|
||||||
items: onlyDirs,
|
|
||||||
collapsed: collapsedSections["folders"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
|
|
||||||
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
|
|
||||||
if (fileItems.length > 0) {
|
|
||||||
fileSections.push({
|
|
||||||
id: "files",
|
|
||||||
title: filesTitle,
|
|
||||||
icon: filesIcon,
|
|
||||||
priority: 4,
|
|
||||||
items: fileItems,
|
|
||||||
collapsed: collapsedSections["files"] || false,
|
|
||||||
flatStartIndex: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newSections;
|
|
||||||
if (searchMode === "files") {
|
|
||||||
newSections = fileSections;
|
|
||||||
} else {
|
|
||||||
var existingNonFile = sections.filter(function (s) {
|
|
||||||
return s.id !== "files" && s.id !== "folders";
|
|
||||||
});
|
|
||||||
newSections = existingNonFile.concat(fileSections);
|
|
||||||
}
|
|
||||||
newSections.sort(function (a, b) {
|
|
||||||
return a.priority - b.priority;
|
|
||||||
});
|
});
|
||||||
_applyHighlights(newSections, searchQuery);
|
}
|
||||||
flatModel = Scorer.flattenSections(newSections);
|
}
|
||||||
sections = newSections;
|
|
||||||
selectedFlatIndex = getFirstItemIndex();
|
function _applyFileSearchResults(fileItems, effectiveType) {
|
||||||
updateSelectedItem();
|
var fileSections = [];
|
||||||
|
var showType = effectiveType;
|
||||||
|
var order = SettingsData.launcherPluginOrder || [];
|
||||||
|
var filesOrderIdx = order.indexOf("__files");
|
||||||
|
var foldersOrderIdx = order.indexOf("__folders");
|
||||||
|
var filesPriority = filesOrderIdx !== -1 ? 2.6 + filesOrderIdx * 0.01 : 4;
|
||||||
|
var foldersPriority = foldersOrderIdx !== -1 ? 2.6 + foldersOrderIdx * 0.01 : 4.1;
|
||||||
|
|
||||||
|
if (showType === "all" && DSearchService.supportsTypeFilter) {
|
||||||
|
var onlyFiles = [];
|
||||||
|
var onlyDirs = [];
|
||||||
|
for (var j = 0; j < fileItems.length; j++) {
|
||||||
|
if (fileItems[j].data?.is_dir)
|
||||||
|
onlyDirs.push(fileItems[j]);
|
||||||
|
else
|
||||||
|
onlyFiles.push(fileItems[j]);
|
||||||
|
}
|
||||||
|
if (onlyFiles.length > 0) {
|
||||||
|
fileSections.push({
|
||||||
|
id: "files",
|
||||||
|
title: I18n.tr("Files"),
|
||||||
|
icon: "insert_drive_file",
|
||||||
|
priority: filesPriority,
|
||||||
|
items: onlyFiles,
|
||||||
|
collapsed: collapsedSections["files"] || false,
|
||||||
|
flatStartIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (onlyDirs.length > 0) {
|
||||||
|
fileSections.push({
|
||||||
|
id: "folders",
|
||||||
|
title: I18n.tr("Folders"),
|
||||||
|
icon: "folder",
|
||||||
|
priority: foldersPriority,
|
||||||
|
items: onlyDirs,
|
||||||
|
collapsed: collapsedSections["folders"] || false,
|
||||||
|
flatStartIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
|
||||||
|
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
|
||||||
|
var singlePriority = showType === "dir" ? foldersPriority : filesPriority;
|
||||||
|
if (fileItems.length > 0) {
|
||||||
|
fileSections.push({
|
||||||
|
id: "files",
|
||||||
|
title: filesTitle,
|
||||||
|
icon: filesIcon,
|
||||||
|
priority: singlePriority,
|
||||||
|
items: fileItems,
|
||||||
|
collapsed: collapsedSections["files"] || false,
|
||||||
|
flatStartIndex: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSections;
|
||||||
|
if (searchMode === "files") {
|
||||||
|
newSections = fileSections;
|
||||||
|
} else {
|
||||||
|
var existingNonFile = sections.filter(function (s) {
|
||||||
|
return s.id !== "files" && s.id !== "folders";
|
||||||
|
});
|
||||||
|
newSections = existingNonFile.concat(fileSections);
|
||||||
|
}
|
||||||
|
newSections.sort(function (a, b) {
|
||||||
|
return a.priority - b.priority;
|
||||||
});
|
});
|
||||||
|
_applyHighlights(newSections, searchQuery);
|
||||||
|
flatModel = Scorer.flattenSections(newSections);
|
||||||
|
sections = newSections;
|
||||||
|
selectedFlatIndex = getFirstItemIndex();
|
||||||
|
updateSelectedItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchApps(query) {
|
function searchApps(query) {
|
||||||
@@ -1276,7 +1310,11 @@ Item {
|
|||||||
function buildDynamicSectionDefs(items) {
|
function buildDynamicSectionDefs(items) {
|
||||||
var baseDefs = sectionDefinitions.slice();
|
var baseDefs = sectionDefinitions.slice();
|
||||||
var pluginSections = {};
|
var pluginSections = {};
|
||||||
var basePriority = 2.6;
|
var order = SettingsData.launcherPluginOrder || [];
|
||||||
|
var orderMap = {};
|
||||||
|
for (var k = 0; k < order.length; k++)
|
||||||
|
orderMap[order[k]] = k;
|
||||||
|
var unorderedPriority = 2.6 + order.length * 0.01;
|
||||||
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
var section = items[i].section;
|
var section = items[i].section;
|
||||||
@@ -1287,19 +1325,25 @@ Item {
|
|||||||
var pluginId = section.substring(7);
|
var pluginId = section.substring(7);
|
||||||
var meta = getPluginMetadata(pluginId);
|
var meta = getPluginMetadata(pluginId);
|
||||||
var viewPref = getPluginViewPref(pluginId);
|
var viewPref = getPluginViewPref(pluginId);
|
||||||
|
var orderIdx = orderMap[pluginId];
|
||||||
|
var priority;
|
||||||
|
if (orderIdx !== undefined) {
|
||||||
|
priority = 2.6 + orderIdx * 0.01;
|
||||||
|
} else {
|
||||||
|
priority = unorderedPriority;
|
||||||
|
unorderedPriority += 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
pluginSections[section] = {
|
pluginSections[section] = {
|
||||||
id: section,
|
id: section,
|
||||||
title: meta.name,
|
title: meta.name,
|
||||||
icon: meta.icon,
|
icon: meta.icon,
|
||||||
priority: basePriority,
|
priority: priority,
|
||||||
defaultViewMode: viewPref.mode || "list"
|
defaultViewMode: viewPref.mode || "list"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (viewPref.mode)
|
if (viewPref.mode)
|
||||||
setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
|
setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
|
||||||
|
|
||||||
basePriority += 0.01;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var sectionId in pluginSections) {
|
for (var sectionId in pluginSections) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Quickshell.Wayland
|
|||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -134,40 +135,47 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function _finishShow(query, mode) {
|
||||||
closeCleanupTimer.stop();
|
spotlightOpen = true;
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
|
||||||
if (focusedScreen)
|
|
||||||
launcherWindow.screen = focusedScreen;
|
|
||||||
|
|
||||||
spotlightOpen = true;
|
|
||||||
keyboardActive = true;
|
keyboardActive = true;
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
_ensureContentLoadedAndInitialize("", "");
|
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
closeCleanupTimer.stop();
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||||
|
spotlightOpen = false;
|
||||||
|
isClosing = false;
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
Qt.callLater(() => root._finishShow("", ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_finishShow("", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithQuery(query) {
|
function showWithQuery(query) {
|
||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
isClosing = false;
|
|
||||||
openedFromOverview = false;
|
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
if (focusedScreen)
|
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||||
|
spotlightOpen = false;
|
||||||
|
isClosing = false;
|
||||||
launcherWindow.screen = focusedScreen;
|
launcherWindow.screen = focusedScreen;
|
||||||
|
Qt.callLater(() => root._finishShow(query, ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spotlightOpen = true;
|
_finishShow(query, "");
|
||||||
keyboardActive = true;
|
|
||||||
ModalManager.openModal(root);
|
|
||||||
if (useHyprlandFocusGrab)
|
|
||||||
focusGrab.active = true;
|
|
||||||
|
|
||||||
_ensureContentLoadedAndInitialize(query, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -191,14 +199,20 @@ Item {
|
|||||||
|
|
||||||
function showWithMode(mode) {
|
function showWithMode(mode) {
|
||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||||
|
spotlightOpen = false;
|
||||||
|
isClosing = false;
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
Qt.callLater(() => root._finishShow("", mode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
|
||||||
if (focusedScreen)
|
|
||||||
launcherWindow.screen = focusedScreen;
|
|
||||||
|
|
||||||
spotlightOpen = true;
|
|
||||||
keyboardActive = true;
|
keyboardActive = true;
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
@@ -295,6 +309,16 @@ Item {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: launcherWindow
|
||||||
|
readonly property real s: Math.min(1, modalContainer.scale)
|
||||||
|
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
|
||||||
|
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
|
||||||
|
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
|
||||||
|
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
|
||||||
|
blurRadius: root.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: {
|
||||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||||
@@ -428,6 +452,14 @@ Item {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: root.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: BlurService.borderColor
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ FocusScope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !editMode
|
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: footerBar
|
id: footerBar
|
||||||
@@ -737,8 +737,6 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ Item {
|
|||||||
height: 24
|
height: 24
|
||||||
z: 100
|
z: 100
|
||||||
visible: {
|
visible: {
|
||||||
|
if (BlurService.enabled)
|
||||||
|
return false;
|
||||||
if (mainListView.contentHeight <= mainListView.height)
|
if (mainListView.contentHeight <= mainListView.height)
|
||||||
return false;
|
return false;
|
||||||
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
||||||
@@ -449,7 +451,7 @@ Item {
|
|||||||
case "apps":
|
case "apps":
|
||||||
return "apps";
|
return "apps";
|
||||||
default:
|
default:
|
||||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
return "search_off";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,9 +487,9 @@ Item {
|
|||||||
case "plugins":
|
case "plugins":
|
||||||
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||||
case "apps":
|
case "apps":
|
||||||
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
return I18n.tr("No apps found");
|
||||||
default:
|
default:
|
||||||
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
return I18n.tr("No results found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ DankPopout {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
property bool isClosing: false
|
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
appDrawerPopout.close();
|
appDrawerPopout.close();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.desktopClockEnabled
|
enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.desktopClockEnabled
|
||||||
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.desktopClockTransparency
|
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.desktopClockTransparency
|
||||||
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.desktopClockColorMode
|
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.desktopClockColorMode
|
||||||
property color customColor: isInstance ? (cfg.customColor ?? "#ffffff") : SettingsData.desktopClockCustomColor
|
property color customColor: isInstance ? (cfg.customColor ?? "#ffffff") : SettingsData.desktopClockCustomColor
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Item {
|
|||||||
readonly property var cfg: instanceData?.config ?? null
|
readonly property var cfg: instanceData?.config ?? null
|
||||||
readonly property bool isInstance: instanceId !== "" && cfg !== null
|
readonly property bool isInstance: instanceId !== "" && cfg !== null
|
||||||
|
|
||||||
property bool enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.systemMonitorEnabled
|
enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.systemMonitorEnabled
|
||||||
property bool showHeader: isInstance ? (cfg.showHeader ?? true) : SettingsData.systemMonitorShowHeader
|
property bool showHeader: isInstance ? (cfg.showHeader ?? true) : SettingsData.systemMonitorShowHeader
|
||||||
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.systemMonitorTransparency
|
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.systemMonitorTransparency
|
||||||
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.systemMonitorColorMode
|
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.systemMonitorColorMode
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ PluginComponent {
|
|||||||
id: detailRoot
|
id: detailRoot
|
||||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -252,7 +252,7 @@ PluginComponent {
|
|||||||
width: parent ? parent.width : 300
|
width: parent ? parent.width : 300
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHighest
|
color: Theme.surfaceLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
opacity: 1.0
|
opacity: 1.0
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ Rectangle {
|
|||||||
property string text: ""
|
property string text: ""
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property int widgetIndex: 0
|
property int widgetIndex: 0
|
||||||
property var widgetData: null
|
property var widgetData: null
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
{
|
{
|
||||||
if (!AudioService.sink)
|
if (!AudioService.sink?.audio)
|
||||||
return "volume_off";
|
return "volume_off";
|
||||||
let volume = AudioService.sink.audio.volume;
|
let volume = AudioService.sink.audio.volume;
|
||||||
let muted = AudioService.sink.audio.muted;
|
let muted = AudioService.sink.audio.muted;
|
||||||
@@ -276,7 +276,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
{
|
{
|
||||||
if (!AudioService.source)
|
if (!AudioService.source?.audio)
|
||||||
return "mic_off";
|
return "mic_off";
|
||||||
let muted = AudioService.source.audio.muted;
|
let muted = AudioService.source.audio.muted;
|
||||||
return muted ? "mic_off" : "mic";
|
return muted ? "mic_off" : "mic";
|
||||||
@@ -369,7 +369,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
{
|
{
|
||||||
if (!AudioService.sink)
|
if (!AudioService.sink?.audio)
|
||||||
return I18n.tr("Select device", "audio status");
|
return I18n.tr("Select device", "audio status");
|
||||||
if (AudioService.sink.audio.muted)
|
if (AudioService.sink.audio.muted)
|
||||||
return I18n.tr("Muted", "audio status");
|
return I18n.tr("Muted", "audio status");
|
||||||
@@ -380,7 +380,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
{
|
{
|
||||||
if (!AudioService.source)
|
if (!AudioService.source?.audio)
|
||||||
return I18n.tr("Select device", "audio status");
|
return I18n.tr("Select device", "audio status");
|
||||||
if (AudioService.source.audio.muted)
|
if (AudioService.source.audio.muted)
|
||||||
return I18n.tr("Muted", "audio status");
|
return I18n.tr("Muted", "audio status");
|
||||||
@@ -412,9 +412,9 @@ Column {
|
|||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
|
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
return !!(AudioService.sink && !AudioService.sink.audio.muted);
|
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
return !!(AudioService.source && !AudioService.source.audio.muted);
|
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Row {
|
|||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Theme.surfaceContainer
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
border.color: Theme.primarySelected
|
border.color: Theme.primarySelected
|
||||||
border.width: 0
|
border.width: 0
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -351,8 +351,8 @@ Rectangle {
|
|||||||
deviceRipple.trigger(mapped.x, mapped.y);
|
deviceRipple.trigger(mapped.x, mapped.y);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData && modelData.name) {
|
||||||
Pipewire.preferredDefaultAudioSource = modelData;
|
AudioService.setDefaultSourceByName(modelData.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,9 +218,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
id: deviceRipple
|
id: deviceRipple
|
||||||
@@ -355,8 +355,8 @@ Rectangle {
|
|||||||
deviceRipple.trigger(mapped.x, mapped.y);
|
deviceRipple.trigger(mapped.x, mapped.y);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData && modelData.name) {
|
||||||
Pipewire.preferredDefaultAudioSink = modelData;
|
AudioService.setDefaultSinkByName(modelData.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,9 +397,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -129,8 +129,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -164,8 +165,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ Item {
|
|||||||
width: 320
|
width: 320
|
||||||
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainer
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
opacity: modalVisible ? 1 : 0
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
@@ -243,8 +242,8 @@ Rectangle {
|
|||||||
if (isConnecting)
|
if (isConnecting)
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||||
if (deviceMouseArea.containsMouse)
|
if (deviceMouseArea.containsMouse)
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
return Theme.primaryHoverLight;
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
|
return Theme.surfaceLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
border.color: {
|
border.color: {
|
||||||
@@ -252,8 +251,9 @@ Rectangle {
|
|||||||
return Theme.warning;
|
return Theme.warning;
|
||||||
if (isConnected)
|
if (isConnected)
|
||||||
return Theme.primary;
|
return Theme.primary;
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12);
|
return Theme.outlineLight;
|
||||||
}
|
}
|
||||||
|
border.width: (isConnecting || isConnected) ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -490,9 +490,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Theme.outlineLight
|
||||||
border.width: 0
|
border.width: 1
|
||||||
opacity: isInteractive ? 1 : 0.6
|
opacity: isInteractive ? 1 : 0.6
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
|
||||||
border.width: modelData.mount === currentMountPath ? 2 : 0
|
border.width: modelData.mount === currentMountPath ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -308,9 +308,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Theme.primary
|
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: isActive ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wiredContentRow
|
id: wiredContentRow
|
||||||
@@ -565,9 +565,9 @@ Rectangle {
|
|||||||
width: wifiContent.width
|
width: wifiContent.width
|
||||||
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: wifiDelegate.isConnected ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wifiContentRow
|
id: wifiContentRow
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSink) {
|
if (defaultSink?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted;
|
defaultSink.audio.muted = !defaultSink.audio.muted;
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSink)
|
if (!defaultSink?.audio)
|
||||||
return "volume_off";
|
return "volume_off";
|
||||||
|
|
||||||
let volume = defaultSink.audio.volume;
|
let volume = defaultSink.audio.volume;
|
||||||
@@ -62,18 +62,18 @@ Row {
|
|||||||
return "volume_up";
|
return "volume_up";
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSink?.audio && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: volumeSlider
|
id: volumeSlider
|
||||||
|
|
||||||
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSink?.audio ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
enabled: defaultSink !== null
|
enabled: defaultSink?.audio != null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: AudioService.sinkMaxVolume
|
maximum: AudioService.sinkMaxVolume
|
||||||
showValue: true
|
showValue: true
|
||||||
@@ -83,7 +83,7 @@ Row {
|
|||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (defaultSink) {
|
if (defaultSink?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSink.audio.volume = newValue / 100.0;
|
defaultSink.audio.volume = newValue / 100.0;
|
||||||
if (newValue > 0 && defaultSink.audio.muted) {
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
@@ -97,7 +97,7 @@ Row {
|
|||||||
Binding {
|
Binding {
|
||||||
target: volumeSlider
|
target: volumeSlider
|
||||||
property: "value"
|
property: "value"
|
||||||
value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
|
value: defaultSink?.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
when: !volumeSlider.isDragging
|
when: !volumeSlider.isDragging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Rectangle {
|
|||||||
property real value: 0.0
|
property real value: 0.0
|
||||||
property real maximumValue: 1.0
|
property real maximumValue: 1.0
|
||||||
property real minimumValue: 0.0
|
property real minimumValue: 0.0
|
||||||
property bool enabled: true
|
|
||||||
|
|
||||||
signal sliderValueChanged(real value)
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSource) {
|
if (defaultSource?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSource.audio.muted = !defaultSource.audio.muted;
|
defaultSource.audio.muted = !defaultSource.audio.muted;
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSource)
|
if (!defaultSource?.audio)
|
||||||
return "mic_off";
|
return "mic_off";
|
||||||
|
|
||||||
let volume = defaultSource.audio.volume;
|
let volume = defaultSource.audio.volume;
|
||||||
@@ -56,26 +56,26 @@ Row {
|
|||||||
return "mic";
|
return "mic";
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSource?.audio && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSource?.audio ? Math.round(defaultSource.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
enabled: defaultSource !== null
|
enabled: defaultSource?.audio != null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
|
value: defaultSource?.audio ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (defaultSource) {
|
if (defaultSource?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSource.audio.volume = newValue / 100.0;
|
defaultSource.audio.volume = newValue / 100.0;
|
||||||
if (newValue > 0 && defaultSource.audio.muted) {
|
if (newValue > 0 && defaultSource.audio.muted) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Rectangle {
|
|||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||||
property bool enabled: BatteryService.batteryAvailable
|
enabled: BatteryService.batteryAvailable
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Rectangle {
|
|||||||
return parseFloat(selectedMount.percent.replace("%", "")) || 0;
|
return parseFloat(selectedMount.percent.replace("%", "")) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool enabled: DgopService.dgopAvailable
|
enabled: DgopService.dgopAvailable
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Rectangle {
|
|||||||
|
|
||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property real iconRotation: 0
|
property real iconRotation: 0
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Rectangle {
|
|||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property string text: ""
|
property string text: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property real iconRotation: 0
|
property real iconRotation: 0
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -357,6 +358,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === centerRepeater.count - 1
|
isLast: index === centerRepeater.count - 1
|
||||||
sectionSpacing: parent.itemSpacing
|
sectionSpacing: parent.itemSpacing
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Item {
|
|||||||
required property var rootWindow
|
required property var rootWindow
|
||||||
required property var barConfig
|
required property var barConfig
|
||||||
|
|
||||||
|
readonly property var blurBarWindow: barWindow
|
||||||
|
|
||||||
property var leftWidgetsModel
|
property var leftWidgetsModel
|
||||||
property var centerWidgetsModel
|
property var centerWidgetsModel
|
||||||
property var rightWidgetsModel
|
property var rightWidgetsModel
|
||||||
@@ -408,6 +410,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hLeftSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
RightSection {
|
RightSection {
|
||||||
id: hRightSection
|
id: hRightSection
|
||||||
@@ -434,6 +442,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hRightSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
CenterSection {
|
CenterSection {
|
||||||
id: hCenterSection
|
id: hCenterSection
|
||||||
@@ -460,6 +474,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hCenterSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -493,6 +513,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: vLeftSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
CenterSection {
|
CenterSection {
|
||||||
id: vCenterSection
|
id: vCenterSection
|
||||||
@@ -520,6 +546,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: vCenterSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
RightSection {
|
RightSection {
|
||||||
id: vRightSection
|
id: vRightSection
|
||||||
@@ -548,6 +580,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: vRightSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,6 +969,7 @@ Item {
|
|||||||
axis: barWindow.axis
|
axis: barWindow.axis
|
||||||
barSpacing: barConfig?.spacing ?? 4
|
barSpacing: barConfig?.spacing ?? 4
|
||||||
barConfig: topBarContent.barConfig
|
barConfig: topBarContent.barConfig
|
||||||
|
widgetData: parent.widgetData
|
||||||
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
|
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
|
||||||
isAtBottom: barWindow.axis?.edge === "bottom"
|
isAtBottom: barWindow.axis?.edge === "bottom"
|
||||||
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
|
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
|
||||||
@@ -1399,12 +1438,21 @@ Item {
|
|||||||
parentScreen: barWindow.screen
|
parentScreen: barWindow.screen
|
||||||
onClicked: {
|
onClicked: {
|
||||||
systemUpdateLoader.active = true;
|
systemUpdateLoader.active = true;
|
||||||
|
if (!systemUpdateLoader.item)
|
||||||
|
return;
|
||||||
|
const popout = systemUpdateLoader.item;
|
||||||
const effectiveBarConfig = topBarContent.barConfig;
|
const effectiveBarConfig = topBarContent.barConfig;
|
||||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||||
if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
|
if (popout.setBarContext) {
|
||||||
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||||
}
|
}
|
||||||
systemUpdateLoader.item?.toggle();
|
if (popout.setTriggerPosition) {
|
||||||
|
const globalPos = visualContent.mapToItem(null, 0, 0);
|
||||||
|
const currentScreen = parentScreen || Screen;
|
||||||
|
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
|
||||||
|
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
|
||||||
|
}
|
||||||
|
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,112 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var blurRegion: null
|
||||||
|
property var _blurWidgetItems: []
|
||||||
|
|
||||||
|
function registerBlurWidget(item) {
|
||||||
|
if (_blurWidgetItems.indexOf(item) >= 0)
|
||||||
|
return;
|
||||||
|
_blurWidgetItems = _blurWidgetItems.concat([item]);
|
||||||
|
_blurRebuildTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterBlurWidget(item) {
|
||||||
|
const idx = _blurWidgetItems.indexOf(item);
|
||||||
|
if (idx < 0)
|
||||||
|
return;
|
||||||
|
const arr = _blurWidgetItems.slice();
|
||||||
|
arr.splice(idx, 1);
|
||||||
|
_blurWidgetItems = arr;
|
||||||
|
_blurRebuildTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _blurRebuildTimer
|
||||||
|
interval: 1
|
||||||
|
onTriggered: barBlur.rebuild()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: barBlur
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
|
||||||
|
|
||||||
|
function rebuild() {
|
||||||
|
teardown();
|
||||||
|
if (!BlurService.enabled || !BlurService.available)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||||
|
const hasBar = barHasTransparency;
|
||||||
|
if (!hasBar && widgets.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const cr = Theme.cornerRadius;
|
||||||
|
let qml = 'import QtQuick; import Quickshell; Region {';
|
||||||
|
for (let i = 0; i < widgets.length; i++) {
|
||||||
|
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
|
||||||
|
}
|
||||||
|
qml += '}';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
|
||||||
|
|
||||||
|
if (hasBar) {
|
||||||
|
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
|
||||||
|
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
|
||||||
|
region.width = Qt.binding(() => barUnitInset.width);
|
||||||
|
region.height = Qt.binding(() => barUnitInset.height);
|
||||||
|
region.radius = Qt.binding(() => barBackground.rt);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < widgets.length; i++) {
|
||||||
|
region[`w${i}`] = widgets[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
barWindow.BackgroundEffect.blurRegion = region;
|
||||||
|
barWindow.blurRegion = region;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("BarBlur: Failed to create blur region:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardown() {
|
||||||
|
if (!barWindow.blurRegion)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
barWindow.BackgroundEffect.blurRegion = null;
|
||||||
|
} catch (e) {}
|
||||||
|
barWindow.blurRegion.destroy();
|
||||||
|
barWindow.blurRegion = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onBarHasTransparencyChanged: _blurRebuildTimer.restart()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: BlurService
|
||||||
|
function onEnabledChanged() {
|
||||||
|
barBlur.rebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: topBarSlide
|
||||||
|
function onXChanged() {
|
||||||
|
if (barWindow.blurRegion)
|
||||||
|
barWindow.blurRegion.changed();
|
||||||
|
}
|
||||||
|
function onYChanged() {
|
||||||
|
if (barWindow.blurRegion)
|
||||||
|
barWindow.blurRegion.changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: rebuild()
|
||||||
|
Component.onDestruction: teardown()
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.layer: dBarLayer
|
WlrLayershell.layer: dBarLayer
|
||||||
WlrLayershell.namespace: "dms:bar"
|
WlrLayershell.namespace: "dms:bar"
|
||||||
|
|
||||||
@@ -711,7 +817,8 @@ PanelWindow {
|
|||||||
onHasActivePopoutChanged: evaluateReveal()
|
onHasActivePopoutChanged: evaluateReveal()
|
||||||
|
|
||||||
function updateActivePopoutState() {
|
function updateActivePopoutState() {
|
||||||
if (!barWindow.screen) return;
|
if (!barWindow.screen)
|
||||||
|
return;
|
||||||
const screenName = barWindow.screen.name;
|
const screenName = barWindow.screen.name;
|
||||||
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
||||||
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === rowRepeater.count - 1
|
isLast: index === rowRepeater.count - 1
|
||||||
sectionSpacing: parent.rowSpacing
|
sectionSpacing: parent.rowSpacing
|
||||||
@@ -103,6 +105,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === columnRepeater.count - 1
|
isLast: index === columnRepeater.count - 1
|
||||||
sectionSpacing: parent.columnSpacing
|
sectionSpacing: parent.columnSpacing
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === rowRepeater.count - 1
|
isLast: index === rowRepeater.count - 1
|
||||||
sectionSpacing: parent.rowSpacing
|
sectionSpacing: parent.rowSpacing
|
||||||
@@ -105,6 +107,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === columnRepeater.count - 1
|
isLast: index === columnRepeater.count - 1
|
||||||
sectionSpacing: parent.columnSpacing
|
sectionSpacing: parent.columnSpacing
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Loader {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool isFirst: false
|
property bool isFirst: false
|
||||||
property bool isLast: false
|
property bool isLast: false
|
||||||
property real sectionSpacing: 0
|
property real sectionSpacing: 0
|
||||||
@@ -92,6 +93,14 @@ Loader {
|
|||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: root.item
|
||||||
|
when: root.item && "blurBarWindow" in root.item
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: root.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: root.item
|
target: root.item
|
||||||
when: root.item && "axis" in root.item
|
when: root.item && "axis" in root.item
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ BasePill {
|
|||||||
if (appItem.isFocused && colorizeEnabled) {
|
if (appItem.isFocused && colorizeEnabled) {
|
||||||
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
|
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
border.width: dragHandler.dragging ? 2 : 0
|
border.width: dragHandler.dragging ? 2 : 0
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell.Services.UPower
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.Plugins
|
import qs.Modules.Plugins
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -10,6 +11,8 @@ BasePill {
|
|||||||
property bool batteryPopupVisible: false
|
property bool batteryPopupVisible: false
|
||||||
property var popoutTarget: null
|
property var popoutTarget: null
|
||||||
|
|
||||||
|
property real touchpadAccumulator: 0
|
||||||
|
|
||||||
readonly property int barPosition: {
|
readonly property int barPosition: {
|
||||||
switch (axis?.edge) {
|
switch (axis?.edge) {
|
||||||
case "top":
|
case "top":
|
||||||
@@ -119,5 +122,44 @@ BasePill {
|
|||||||
battery.triggerRipple(this, mouse.x, mouse.y);
|
battery.triggerRipple(this, mouse.x, mouse.y);
|
||||||
toggleBatteryPopup();
|
toggleBatteryPopup();
|
||||||
}
|
}
|
||||||
|
onWheel: wheel => {
|
||||||
|
var delta = wheel.angleDelta.y;
|
||||||
|
if (delta === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if this is a touchpad
|
||||||
|
if (delta !== 120 && delta !== -120) {
|
||||||
|
touchpadAccumulator += delta;
|
||||||
|
console.info("Acc: "+touchpadAccumulator);
|
||||||
|
if (Math.abs(touchpadAccumulator) < 500)
|
||||||
|
return;
|
||||||
|
delta = touchpadAccumulator;
|
||||||
|
touchpadAccumulator = 0;
|
||||||
|
}
|
||||||
|
console.info("Trigger! Delta: "+delta)
|
||||||
|
|
||||||
|
// This is after the other delta checks so it only shows on valid Y scroll
|
||||||
|
if (typeof PowerProfiles === "undefined") {
|
||||||
|
ToastService.showError("power-profiles-daemon not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of profiles, and current index
|
||||||
|
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
|
||||||
|
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
|
||||||
|
|
||||||
|
// Step once based on mouse wheel direction
|
||||||
|
if (delta > 0) index += 1;
|
||||||
|
else index -= 1;
|
||||||
|
|
||||||
|
// Already at end of list, can't go further
|
||||||
|
if (index < 0 || index >= profiles.length) return;
|
||||||
|
|
||||||
|
// Set new profile
|
||||||
|
PowerProfiles.profile = profiles[index];
|
||||||
|
if (PowerProfiles.profile !== profiles[index]) {
|
||||||
|
ToastService.showError("Failed to set power profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Quickshell
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.Plugins
|
import qs.Modules.Plugins
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
BasePill {
|
BasePill {
|
||||||
@@ -93,6 +94,15 @@ BasePill {
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: contextMenuWindow
|
id: contextMenuWindow
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: contextMenuWindow
|
||||||
|
blurX: menuContainer.x
|
||||||
|
blurY: menuContainer.y
|
||||||
|
blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
|
||||||
|
blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:clipboard-context-menu"
|
WlrLayershell.namespace: "dms:clipboard-context-menu"
|
||||||
|
|
||||||
property bool isVertical: false
|
property bool isVertical: false
|
||||||
@@ -187,8 +197,8 @@ BasePill {
|
|||||||
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
|
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 1
|
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||||
|
|
||||||
opacity: contextMenuWindow.visible ? 1 : 0
|
opacity: contextMenuWindow.visible ? 1 : 0
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
@@ -224,7 +234,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -264,7 +274,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ BasePill {
|
|||||||
StyledTextMetrics {
|
StyledTextMetrics {
|
||||||
id: cpuBaseline
|
id: cpuBaseline
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
text: "88%"
|
text: "100%"
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledTextMetrics {
|
StyledTextMetrics {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ BasePill {
|
|||||||
property int availableWidth: 400
|
property int availableWidth: 400
|
||||||
readonly property int maxNormalWidth: 456
|
readonly property int maxNormalWidth: 456
|
||||||
readonly property int maxCompactWidth: 288
|
readonly property int maxCompactWidth: 288
|
||||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
property Toplevel activeWindow: null
|
||||||
property var activeDesktopEntry: null
|
property var activeDesktopEntry: null
|
||||||
property bool isHovered: mouseArea.containsMouse
|
property bool isHovered: mouseArea.containsMouse
|
||||||
property bool isAutoHideBar: false
|
property bool isAutoHideBar: false
|
||||||
@@ -38,10 +38,44 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateActiveWindow() {
|
||||||
|
const active = ToplevelManager.activeToplevel;
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
// Only clear if our tracked window is no longer alive
|
||||||
|
if (activeWindow) {
|
||||||
|
const alive = ToplevelManager.toplevels?.values;
|
||||||
|
if (alive && !Array.from(alive).some(t => t === activeWindow))
|
||||||
|
activeWindow = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) {
|
||||||
|
activeWindow = active;
|
||||||
|
}
|
||||||
|
// else: active window is on a different screen so keep the previous value
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
updateActiveWindow();
|
||||||
updateDesktopEntry();
|
updateDesktopEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ToplevelManager
|
||||||
|
function onActiveToplevelChanged() {
|
||||||
|
root.updateActiveWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: CompositorService
|
||||||
|
function onToplevelsChanged() {
|
||||||
|
root.updateActiveWindow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DesktopEntries
|
target: DesktopEntries
|
||||||
function onApplicationsChanged() {
|
function onApplicationsChanged() {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ BasePill {
|
|||||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||||
property bool compactMode: false
|
property bool compactMode: false
|
||||||
property var widgetData: null
|
property var widgetData: null
|
||||||
readonly property int textWidth: {
|
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
|
||||||
|
readonly property int maxTextWidth: {
|
||||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -36,10 +37,7 @@ BasePill {
|
|||||||
if (isVerticalOrientation) {
|
if (isVerticalOrientation) {
|
||||||
return widgetThickness - horizontalPadding * 2;
|
return widgetThickness - horizontalPadding * 2;
|
||||||
}
|
}
|
||||||
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
|
return 0;
|
||||||
const audioVizWidth = 20;
|
|
||||||
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
|
||||||
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
|
|
||||||
}
|
}
|
||||||
readonly property int currentContentHeight: {
|
readonly property int currentContentHeight: {
|
||||||
if (!isVerticalOrientation) {
|
if (!isVerticalOrientation) {
|
||||||
@@ -99,7 +97,7 @@ BasePill {
|
|||||||
|
|
||||||
if (isMouseWheelY) {
|
if (isMouseWheelY) {
|
||||||
if (deltaY > 0) {
|
if (deltaY > 0) {
|
||||||
activePlayer.previous();
|
MprisController.previousOrRewind();
|
||||||
} else {
|
} else {
|
||||||
activePlayer.next();
|
activePlayer.next();
|
||||||
}
|
}
|
||||||
@@ -107,7 +105,7 @@ BasePill {
|
|||||||
scrollAccumulatorY += deltaY;
|
scrollAccumulatorY += deltaY;
|
||||||
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
||||||
if (scrollAccumulatorY > 0) {
|
if (scrollAccumulatorY > 0) {
|
||||||
activePlayer.previous();
|
MprisController.previousOrRewind();
|
||||||
} else {
|
} else {
|
||||||
activePlayer.next();
|
activePlayer.next();
|
||||||
}
|
}
|
||||||
@@ -119,7 +117,28 @@ BasePill {
|
|||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Item {
|
Item {
|
||||||
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
|
id: contentRoot
|
||||||
|
readonly property real measuredTextWidth: {
|
||||||
|
if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
|
||||||
|
return 0;
|
||||||
|
// Preserve the fixed-width text slot even if metadata is briefly empty.
|
||||||
|
if (!root.adaptiveWidthEnabled)
|
||||||
|
return root.maxTextWidth;
|
||||||
|
if (textContainer.displayText.length === 0)
|
||||||
|
return 0;
|
||||||
|
const rawWidth = mediaText.contentWidth;
|
||||||
|
if (!isFinite(rawWidth) || rawWidth <= 0)
|
||||||
|
return 0;
|
||||||
|
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
|
||||||
|
}
|
||||||
|
readonly property int horizontalContentWidth: {
|
||||||
|
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
|
||||||
|
const audioVizWidth = 20;
|
||||||
|
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
||||||
|
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
|
||||||
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
||||||
opacity: root.playerAvailable ? 1 : 0
|
opacity: root.playerAvailable ? 1 : 0
|
||||||
|
|
||||||
@@ -132,8 +151,9 @@ BasePill {
|
|||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.mediumDuration
|
||||||
easing.type: Theme.standardEasing
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +234,7 @@ BasePill {
|
|||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
activePlayer.togglePlaying();
|
activePlayer.togglePlaying();
|
||||||
} else if (mouse.button === Qt.MiddleButton) {
|
} else if (mouse.button === Qt.MiddleButton) {
|
||||||
activePlayer.previous();
|
MprisController.previousOrRewind();
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
} else if (mouse.button === Qt.RightButton) {
|
||||||
activePlayer.next();
|
activePlayer.next();
|
||||||
}
|
}
|
||||||
@@ -269,7 +289,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: textWidth
|
width: contentRoot.measuredTextWidth
|
||||||
height: root.widgetThickness
|
height: root.widgetThickness
|
||||||
visible: {
|
visible: {
|
||||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||||
@@ -278,50 +298,95 @@ BasePill {
|
|||||||
clip: true
|
clip: true
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
StyledText {
|
Behavior on width {
|
||||||
id: mediaText
|
NumberAnimation {
|
||||||
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
|
duration: Theme.mediumDuration
|
||||||
property real scrollOffset: 0
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: textContainer.displayText
|
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
|
||||||
color: Theme.widgetTextColor
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
x: needsScrolling ? -scrollOffset : 0
|
|
||||||
onTextChanged: {
|
|
||||||
scrollOffset = 0;
|
|
||||||
scrollAnimation.restart();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
Item {
|
||||||
id: scrollAnimation
|
id: textClip
|
||||||
running: mediaText.needsScrolling && textContainer.visible
|
anchors.fill: parent
|
||||||
loops: Animation.Infinite
|
clip: true
|
||||||
|
|
||||||
PauseAnimation {
|
StyledText {
|
||||||
duration: 2000
|
id: mediaText
|
||||||
|
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
|
||||||
|
property real scrollOffset: 0
|
||||||
|
property real textShift: 0
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: textContainer.displayText
|
||||||
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
|
color: Theme.widgetTextColor
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
x: (needsScrolling ? -scrollOffset : 0) + textShift
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
scrollOffset = 0;
|
||||||
|
textShift = 0;
|
||||||
|
scrollAnimation.restart();
|
||||||
|
textChangeAnimation.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
SequentialAnimation {
|
||||||
target: mediaText
|
id: scrollAnimation
|
||||||
property: "scrollOffset"
|
running: mediaText.needsScrolling && textContainer.visible
|
||||||
from: 0
|
loops: Animation.Infinite
|
||||||
to: mediaText.implicitWidth - textContainer.width + 5
|
|
||||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
PauseAnimation {
|
||||||
easing.type: Easing.Linear
|
duration: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: mediaText
|
||||||
|
property: "scrollOffset"
|
||||||
|
from: 0
|
||||||
|
to: mediaText.implicitWidth - textContainer.width + 5
|
||||||
|
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
|
||||||
|
PauseAnimation {
|
||||||
|
duration: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: mediaText
|
||||||
|
property: "scrollOffset"
|
||||||
|
to: 0
|
||||||
|
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PauseAnimation {
|
SequentialAnimation {
|
||||||
duration: 2000
|
id: textChangeAnimation
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
ParallelAnimation {
|
||||||
target: mediaText
|
NumberAnimation {
|
||||||
property: "scrollOffset"
|
target: mediaText
|
||||||
to: 0
|
property: "opacity"
|
||||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
from: 0.7
|
||||||
easing.type: Easing.Linear
|
to: 1
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: mediaText
|
||||||
|
property: "textShift"
|
||||||
|
from: 4
|
||||||
|
to: 0
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,7 +419,7 @@ BasePill {
|
|||||||
height: 20
|
height: 20
|
||||||
radius: 10
|
radius: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
visible: root.playerAvailable
|
visible: root.playerAvailable
|
||||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
||||||
|
|
||||||
@@ -370,11 +435,7 @@ BasePill {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.playerAvailable
|
enabled: root.playerAvailable
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: MprisController.previousOrRewind()
|
||||||
if (activePlayer) {
|
|
||||||
activePlayer.previous();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,7 +472,7 @@ BasePill {
|
|||||||
height: 20
|
height: 20
|
||||||
radius: 10
|
radius: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
visible: playerAvailable
|
visible: playerAvailable
|
||||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: tabArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -327,7 +327,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: newNoteArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ BasePill {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
// App icon
|
// App icon
|
||||||
@@ -526,9 +526,9 @@ BasePill {
|
|||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||||
}
|
}
|
||||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
@@ -738,6 +738,15 @@ BasePill {
|
|||||||
sourceComponent: PanelWindow {
|
sourceComponent: PanelWindow {
|
||||||
id: contextMenuWindow
|
id: contextMenuWindow
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: contextMenuWindow
|
||||||
|
blurX: contextMenuRect.x
|
||||||
|
blurY: contextMenuRect.y
|
||||||
|
blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
|
||||||
|
blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
property var currentWindow: null
|
property var currentWindow: null
|
||||||
property bool isVisible: false
|
property bool isVisible: false
|
||||||
property point anchorPos: Qt.point(0, 0)
|
property point anchorPos: Qt.point(0, 0)
|
||||||
@@ -830,6 +839,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: contextMenuRect
|
||||||
x: {
|
x: {
|
||||||
if (contextMenuWindow.isVertical) {
|
if (contextMenuWindow.isVertical) {
|
||||||
if (contextMenuWindow.edge === "left") {
|
if (contextMenuWindow.edge === "left") {
|
||||||
@@ -858,13 +868,13 @@ BasePill {
|
|||||||
height: 32
|
height: 32
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 1
|
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ BasePill {
|
|||||||
enableCursor: false
|
enableCursor: false
|
||||||
|
|
||||||
property var parentWindow: null
|
property var parentWindow: null
|
||||||
|
property var widgetData: null
|
||||||
|
property string section: "right"
|
||||||
property bool isAtBottom: false
|
property bool isAtBottom: false
|
||||||
property bool isAutoHideBar: false
|
property bool isAutoHideBar: false
|
||||||
|
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
|
||||||
readonly property var hiddenTrayIds: {
|
readonly property var hiddenTrayIds: {
|
||||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
||||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
||||||
@@ -40,6 +43,76 @@ BasePill {
|
|||||||
return `${id}::${tooltipTitle}`;
|
return `${id}::${tooltipTitle}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trayIconSourceFor(trayItem) {
|
||||||
|
let icon = trayItem && trayItem.icon;
|
||||||
|
if (typeof icon === 'string' || icon instanceof String) {
|
||||||
|
if (icon === "")
|
||||||
|
return "";
|
||||||
|
if (icon.includes("?path=")) {
|
||||||
|
const split = icon.split("?path=");
|
||||||
|
if (split.length !== 2)
|
||||||
|
return icon;
|
||||||
|
const name = split[0];
|
||||||
|
const path = split[1];
|
||||||
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||||
|
if (fileName.startsWith("dropboxstatus")) {
|
||||||
|
fileName = `hicolor/16x16/status/${fileName}`;
|
||||||
|
}
|
||||||
|
return `file://${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
||||||
|
return `file://${icon}`;
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateInlineTrayItem(trayItem, anchorItem) {
|
||||||
|
if (!trayItem)
|
||||||
|
return;
|
||||||
|
if (!trayItem.onlyMenu) {
|
||||||
|
trayItem.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trayItem.hasMenu)
|
||||||
|
return;
|
||||||
|
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
|
||||||
|
if (!trayItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trayItem.hasMenu) {
|
||||||
|
const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
|
||||||
|
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleIconName() {
|
||||||
|
const edge = root.axis?.edge;
|
||||||
|
if (root.useOverflowPopup) {
|
||||||
|
switch (edge) {
|
||||||
|
case "left":
|
||||||
|
return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
|
||||||
|
case "right":
|
||||||
|
return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
|
||||||
|
case "bottom":
|
||||||
|
return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
|
||||||
|
case "top":
|
||||||
|
return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge === "left" || edge === "right") {
|
||||||
|
return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
|
||||||
|
}
|
||||||
|
|
||||||
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
||||||
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
||||||
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
||||||
@@ -78,6 +151,13 @@ BasePill {
|
|||||||
item: item
|
item: item
|
||||||
}))
|
}))
|
||||||
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
|
||||||
|
readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
|
||||||
|
readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
|
||||||
|
readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
|
||||||
|
readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
|
||||||
|
key: getTrayItemKey(item),
|
||||||
|
item: item
|
||||||
|
}))
|
||||||
|
|
||||||
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
|
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
|
||||||
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
|
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
|
||||||
@@ -103,6 +183,7 @@ BasePill {
|
|||||||
property int dropTargetIndex: -1
|
property int dropTargetIndex: -1
|
||||||
property bool suppressShiftAnimation: false
|
property bool suppressShiftAnimation: false
|
||||||
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
||||||
|
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
|
||||||
visible: allTrayItems.length > 0
|
visible: allTrayItems.length > 0
|
||||||
opacity: allTrayItems.length > 0 ? 1 : 0
|
opacity: allTrayItems.length > 0 ? 1 : 0
|
||||||
|
|
||||||
@@ -198,10 +279,11 @@ BasePill {
|
|||||||
id: rowComp
|
id: rowComp
|
||||||
Row {
|
Row {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: root.mainBarItems
|
values: root.displayedMainBarItems
|
||||||
objectProp: "key"
|
objectProp: "key"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,29 +291,7 @@ BasePill {
|
|||||||
id: delegateRoot
|
id: delegateRoot
|
||||||
property var trayItem: modelData.item
|
property var trayItem: modelData.item
|
||||||
property string itemKey: modelData.key
|
property string itemKey: modelData.key
|
||||||
property string iconSource: {
|
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "")
|
|
||||||
return "";
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2)
|
|
||||||
return icon;
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
|
||||||
return `file://${icon}`;
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.trayItemSize
|
width: root.trayItemSize
|
||||||
height: root.barThickness
|
height: root.barThickness
|
||||||
@@ -287,7 +347,7 @@ BasePill {
|
|||||||
height: root.trayItemSize
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
border.width: dragHandler.dragging ? 2 : 0
|
border.width: dragHandler.dragging ? 2 : 0
|
||||||
border.color: Theme.primary
|
border.color: Theme.primary
|
||||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||||
@@ -371,7 +431,8 @@ BasePill {
|
|||||||
}
|
}
|
||||||
if (!delegateRoot.trayItem.hasMenu)
|
if (!delegateRoot.trayItem.hasMenu)
|
||||||
return;
|
return;
|
||||||
root.menuOpen = false;
|
if (root.useOverflowPopup)
|
||||||
|
root.menuOpen = false;
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,8 +441,8 @@ BasePill {
|
|||||||
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
||||||
if (distance > 5) {
|
if (distance > 5) {
|
||||||
dragHandler.dragging = true;
|
dragHandler.dragging = true;
|
||||||
root.draggedIndex = index;
|
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
|
||||||
root.dropTargetIndex = index;
|
root.dropTargetIndex = root.draggedIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!dragHandler.dragging)
|
if (!dragHandler.dragging)
|
||||||
@@ -391,7 +452,8 @@ BasePill {
|
|||||||
dragHandler.dragAxisOffset = axisOffset;
|
dragHandler.dragAxisOffset = axisOffset;
|
||||||
const itemSize = root.trayItemSize;
|
const itemSize = root.trayItemSize;
|
||||||
const slotOffset = Math.round(axisOffset / itemSize);
|
const slotOffset = Math.round(axisOffset / itemSize);
|
||||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||||
|
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
|
||||||
if (newTargetIndex !== root.dropTargetIndex) {
|
if (newTargetIndex !== root.dropTargetIndex) {
|
||||||
root.dropTargetIndex = newTargetIndex;
|
root.dropTargetIndex = newTargetIndex;
|
||||||
}
|
}
|
||||||
@@ -407,7 +469,8 @@ BasePill {
|
|||||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
root.menuOpen = false;
|
if (root.useOverflowPopup)
|
||||||
|
root.menuOpen = false;
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,11 +488,11 @@ BasePill {
|
|||||||
height: root.trayItemSize
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: root.menuOpen ? "expand_less" : "expand_more"
|
name: root.toggleIconName()
|
||||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
@@ -451,6 +514,301 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.displayedInlineExpandedItems
|
||||||
|
objectProp: "key"
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: inlineExpandedTrayItemDelegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: inlineExpandedTrayItemDelegate
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property var trayItem: modelData.item
|
||||||
|
property string itemKey: modelData.key
|
||||||
|
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||||
|
|
||||||
|
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
|
||||||
|
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
|
||||||
|
visible: width > 0 || height > 0
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
enabled: !root.isVerticalOrientation
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
enabled: root.isVerticalOrientation
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: inlineVisualContent
|
||||||
|
width: root.trayItemSize
|
||||||
|
height: root.trayItemSize
|
||||||
|
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
|
||||||
|
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
opacity: root.inlineExpanded ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: inlineIconImg
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
source: iconSource
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !inlineIconImg.visible
|
||||||
|
text: {
|
||||||
|
const itemId = trayItem?.id || "";
|
||||||
|
if (!itemId)
|
||||||
|
return "?";
|
||||||
|
return itemId.charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: Theme.widgetTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
DankRipple {
|
||||||
|
id: inlineItemRipple
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: inlineTrayItemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: root.inlineExpanded
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
|
||||||
|
inlineItemRipple.trigger(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
root.activateInlineTrayItem(trayItem, inlineVisualContent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mouse.button !== Qt.RightButton)
|
||||||
|
return;
|
||||||
|
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: verticalMainTrayItemDelegate
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property var trayItem: modelData.item
|
||||||
|
property string itemKey: modelData.key
|
||||||
|
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||||
|
|
||||||
|
width: root.barThickness
|
||||||
|
height: root.trayItemSize
|
||||||
|
z: dragHandler.dragging ? 100 : 0
|
||||||
|
|
||||||
|
property real shiftOffset: {
|
||||||
|
if (root.draggedIndex < 0)
|
||||||
|
return 0;
|
||||||
|
if (index === root.draggedIndex)
|
||||||
|
return 0;
|
||||||
|
const dragIdx = root.draggedIndex;
|
||||||
|
const dropIdx = root.dropTargetIndex;
|
||||||
|
const shiftAmount = root.trayItemSize;
|
||||||
|
if (dropIdx < 0)
|
||||||
|
return 0;
|
||||||
|
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||||
|
return -shiftAmount;
|
||||||
|
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
||||||
|
return shiftAmount;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
y: shiftOffset
|
||||||
|
Behavior on y {
|
||||||
|
enabled: !root.suppressShiftAnimation
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dragHandler
|
||||||
|
anchors.fill: parent
|
||||||
|
property bool dragging: false
|
||||||
|
property point dragStartPos: Qt.point(0, 0)
|
||||||
|
property real dragAxisOffset: 0
|
||||||
|
property bool longPressing: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: longPressTimer
|
||||||
|
interval: 400
|
||||||
|
repeat: false
|
||||||
|
onTriggered: dragHandler.longPressing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: visualContent
|
||||||
|
width: root.trayItemSize
|
||||||
|
height: root.trayItemSize
|
||||||
|
anchors.centerIn: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
border.width: dragHandler.dragging ? 2 : 0
|
||||||
|
border.color: Theme.primary
|
||||||
|
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: iconImg
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
|
source: iconSource
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !iconImg.visible
|
||||||
|
text: {
|
||||||
|
const itemId = trayItem?.id || "";
|
||||||
|
if (!itemId)
|
||||||
|
return "?";
|
||||||
|
return itemId.charAt(0).toUpperCase();
|
||||||
|
}
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: Theme.widgetTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
DankRipple {
|
||||||
|
id: itemRipple
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: trayItemArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
||||||
|
itemRipple.trigger(pos.x, pos.y);
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||||
|
longPressTimer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: mouse => {
|
||||||
|
longPressTimer.stop();
|
||||||
|
const wasDragging = dragHandler.dragging;
|
||||||
|
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
||||||
|
|
||||||
|
if (didReorder) {
|
||||||
|
root.suppressShiftAnimation = true;
|
||||||
|
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
||||||
|
Qt.callLater(() => root.suppressShiftAnimation = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragHandler.longPressing = false;
|
||||||
|
dragHandler.dragging = false;
|
||||||
|
dragHandler.dragAxisOffset = 0;
|
||||||
|
root.draggedIndex = -1;
|
||||||
|
root.dropTargetIndex = -1;
|
||||||
|
|
||||||
|
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!trayItem)
|
||||||
|
return;
|
||||||
|
if (!trayItem.onlyMenu) {
|
||||||
|
trayItem.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!trayItem.hasMenu)
|
||||||
|
return;
|
||||||
|
if (root.useOverflowPopup)
|
||||||
|
root.menuOpen = false;
|
||||||
|
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (dragHandler.longPressing && !dragHandler.dragging) {
|
||||||
|
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
||||||
|
if (distance > 5) {
|
||||||
|
dragHandler.dragging = true;
|
||||||
|
root.draggedIndex = index;
|
||||||
|
root.dropTargetIndex = root.draggedIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dragHandler.dragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
||||||
|
dragHandler.dragAxisOffset = axisOffset;
|
||||||
|
const itemSize = root.trayItemSize;
|
||||||
|
const slotOffset = Math.round(axisOffset / itemSize);
|
||||||
|
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||||
|
if (newTargetIndex !== root.dropTargetIndex) {
|
||||||
|
root.dropTargetIndex = newTargetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (dragHandler.dragging)
|
||||||
|
return;
|
||||||
|
if (mouse.button !== Qt.RightButton)
|
||||||
|
return;
|
||||||
|
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,219 +817,23 @@ BasePill {
|
|||||||
Column {
|
Column {
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
// Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
|
||||||
|
// When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
|
||||||
|
// Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
values: root.mainBarItems
|
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
|
||||||
objectProp: "key"
|
objectProp: "key"
|
||||||
}
|
}
|
||||||
|
delegate: verticalMainTrayItemDelegate
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Item {
|
Repeater {
|
||||||
id: delegateRoot
|
model: ScriptModel {
|
||||||
property var trayItem: modelData.item
|
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
|
||||||
property string itemKey: modelData.key
|
objectProp: "key"
|
||||||
property string iconSource: {
|
|
||||||
let icon = trayItem && trayItem.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "")
|
|
||||||
return "";
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2)
|
|
||||||
return icon;
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
|
||||||
return `file://${icon}`;
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.barThickness
|
|
||||||
height: root.trayItemSize
|
|
||||||
z: dragHandler.dragging ? 100 : 0
|
|
||||||
|
|
||||||
property real shiftOffset: {
|
|
||||||
if (root.draggedIndex < 0)
|
|
||||||
return 0;
|
|
||||||
if (index === root.draggedIndex)
|
|
||||||
return 0;
|
|
||||||
const dragIdx = root.draggedIndex;
|
|
||||||
const dropIdx = root.dropTargetIndex;
|
|
||||||
const shiftAmount = root.trayItemSize;
|
|
||||||
if (dropIdx < 0)
|
|
||||||
return 0;
|
|
||||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
|
||||||
return -shiftAmount;
|
|
||||||
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
|
||||||
return shiftAmount;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
y: delegateRoot.shiftOffset
|
|
||||||
Behavior on y {
|
|
||||||
enabled: !root.suppressShiftAnimation
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: dragHandler
|
|
||||||
anchors.fill: parent
|
|
||||||
property bool dragging: false
|
|
||||||
property point dragStartPos: Qt.point(0, 0)
|
|
||||||
property real dragAxisOffset: 0
|
|
||||||
property bool longPressing: false
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: longPressTimer
|
|
||||||
interval: 400
|
|
||||||
repeat: false
|
|
||||||
onTriggered: dragHandler.longPressing = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: visualContent
|
|
||||||
width: root.trayItemSize
|
|
||||||
height: root.trayItemSize
|
|
||||||
anchors.centerIn: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
|
||||||
border.width: dragHandler.dragging ? 2 : 0
|
|
||||||
border.color: Theme.primary
|
|
||||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
id: iconImg
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
|
||||||
source: delegateRoot.iconSource
|
|
||||||
asynchronous: true
|
|
||||||
smooth: true
|
|
||||||
mipmap: true
|
|
||||||
visible: status === Image.Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !iconImg.visible
|
|
||||||
text: {
|
|
||||||
const itemId = trayItem?.id || "";
|
|
||||||
if (!itemId)
|
|
||||||
return "?";
|
|
||||||
return itemId.charAt(0).toUpperCase();
|
|
||||||
}
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: Theme.widgetTextColor
|
|
||||||
}
|
|
||||||
|
|
||||||
DankRipple {
|
|
||||||
id: itemRipple
|
|
||||||
cornerRadius: Theme.cornerRadius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: trayItemArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
||||||
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onPressed: mouse => {
|
|
||||||
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
|
||||||
itemRipple.trigger(pos.x, pos.y);
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
|
||||||
longPressTimer.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onReleased: mouse => {
|
|
||||||
longPressTimer.stop();
|
|
||||||
const wasDragging = dragHandler.dragging;
|
|
||||||
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
|
||||||
|
|
||||||
if (didReorder) {
|
|
||||||
root.suppressShiftAnimation = true;
|
|
||||||
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
|
||||||
Qt.callLater(() => root.suppressShiftAnimation = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragHandler.longPressing = false;
|
|
||||||
dragHandler.dragging = false;
|
|
||||||
dragHandler.dragAxisOffset = 0;
|
|
||||||
root.draggedIndex = -1;
|
|
||||||
root.dropTargetIndex = -1;
|
|
||||||
|
|
||||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!delegateRoot.trayItem)
|
|
||||||
return;
|
|
||||||
if (!delegateRoot.trayItem.onlyMenu) {
|
|
||||||
delegateRoot.trayItem.activate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!delegateRoot.trayItem.hasMenu)
|
|
||||||
return;
|
|
||||||
root.menuOpen = false;
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (dragHandler.longPressing && !dragHandler.dragging) {
|
|
||||||
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
|
||||||
if (distance > 5) {
|
|
||||||
dragHandler.dragging = true;
|
|
||||||
root.draggedIndex = index;
|
|
||||||
root.dropTargetIndex = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!dragHandler.dragging)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
|
||||||
dragHandler.dragAxisOffset = axisOffset;
|
|
||||||
const itemSize = root.trayItemSize;
|
|
||||||
const slotOffset = Math.round(axisOffset / itemSize);
|
|
||||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
|
||||||
if (newTargetIndex !== root.dropTargetIndex) {
|
|
||||||
root.dropTargetIndex = newTargetIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: mouse => {
|
|
||||||
if (dragHandler.dragging)
|
|
||||||
return;
|
|
||||||
if (mouse.button !== Qt.RightButton)
|
|
||||||
return;
|
|
||||||
if (!delegateRoot.trayItem?.hasMenu) {
|
|
||||||
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
|
||||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
root.menuOpen = false;
|
|
||||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
delegate: inlineExpandedTrayItemDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -685,18 +847,11 @@ BasePill {
|
|||||||
height: root.trayItemSize
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: root.toggleIconName()
|
||||||
const edge = root.axis?.edge;
|
|
||||||
if (edge === "left") {
|
|
||||||
return root.menuOpen ? "chevron_left" : "chevron_right";
|
|
||||||
} else {
|
|
||||||
return root.menuOpen ? "chevron_right" : "chevron_left";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
@@ -718,12 +873,38 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
|
||||||
|
objectProp: "key"
|
||||||
|
}
|
||||||
|
delegate: inlineExpandedTrayItemDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.reverseInlineVertical ? root.displayedMainBarItems : []
|
||||||
|
objectProp: "key"
|
||||||
|
}
|
||||||
|
delegate: verticalMainTrayItemDelegate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: overflowMenu
|
id: overflowMenu
|
||||||
visible: root.menuOpen
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: overflowMenu
|
||||||
|
blurX: menuContainer.x
|
||||||
|
blurY: menuContainer.y
|
||||||
|
blurWidth: root.menuOpen ? menuContainer.width : 0
|
||||||
|
blurHeight: root.menuOpen ? menuContainer.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: root.useOverflowPopup && root.menuOpen
|
||||||
screen: root.parentScreen
|
screen: root.parentScreen
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: WlrLayershell.Top
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
@@ -739,13 +920,14 @@ BasePill {
|
|||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
windows: [overflowMenu]
|
windows: [overflowMenu]
|
||||||
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
|
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: PopoutManager
|
target: PopoutManager
|
||||||
function onPopoutOpening() {
|
function onPopoutOpening() {
|
||||||
root.menuOpen = false;
|
if (root.useOverflowPopup)
|
||||||
|
root.menuOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,6 +1172,15 @@ BasePill {
|
|||||||
layer.samples: 4
|
layer.samples: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: BlurService.borderColor
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
|
|
||||||
Grid {
|
Grid {
|
||||||
id: menuGrid
|
id: menuGrid
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -1002,35 +1193,12 @@ BasePill {
|
|||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
property var trayItem: modelData
|
property var trayItem: modelData
|
||||||
property string iconSource: {
|
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||||
let icon = trayItem?.icon;
|
|
||||||
if (typeof icon === 'string' || icon instanceof String) {
|
|
||||||
if (icon === "")
|
|
||||||
return "";
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const split = icon.split("?path=");
|
|
||||||
if (split.length !== 2)
|
|
||||||
return icon;
|
|
||||||
const name = split[0];
|
|
||||||
const path = split[1];
|
|
||||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
if (fileName.startsWith("dropboxstatus")) {
|
|
||||||
fileName = `hicolor/16x16/status/${fileName}`;
|
|
||||||
}
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
|
||||||
return `file://${icon}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
width: root.trayItemSize + 4
|
width: root.trayItemSize + 4
|
||||||
height: root.trayItemSize + 4
|
height: root.trayItemSize + 4
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: menuIconImg
|
id: menuIconImg
|
||||||
@@ -1191,6 +1359,15 @@ BasePill {
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: menuWindow
|
id: menuWindow
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: menuWindow
|
||||||
|
blurX: trayMenuContainer.x
|
||||||
|
blurY: trayMenuContainer.y
|
||||||
|
blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
|
||||||
|
blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:tray-menu-window"
|
WlrLayershell.namespace: "dms:tray-menu-window"
|
||||||
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: WlrLayershell.Top
|
||||||
@@ -1285,7 +1462,8 @@ BasePill {
|
|||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
updatePosition();
|
updatePosition();
|
||||||
root.menuOpen = false;
|
if (root.useOverflowPopup)
|
||||||
|
root.menuOpen = false;
|
||||||
PopoutManager.closeAllPopouts();
|
PopoutManager.closeAllPopouts();
|
||||||
ModalManager.closeAllModalsExcept(null);
|
ModalManager.closeAllModalsExcept(null);
|
||||||
}
|
}
|
||||||
@@ -1302,7 +1480,7 @@ BasePill {
|
|||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
const clickX = mouse.x + menuWindow.maskX;
|
const clickX = mouse.x + menuWindow.maskX;
|
||||||
const clickY = mouse.y + menuWindow.maskY;
|
const clickY = mouse.y + menuWindow.maskY;
|
||||||
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
|
const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height;
|
||||||
|
|
||||||
if (!outsideContent)
|
if (!outsideContent)
|
||||||
return;
|
return;
|
||||||
@@ -1360,7 +1538,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: menuContainer
|
id: trayMenuContainer
|
||||||
|
|
||||||
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
|
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
|
||||||
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
|
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||||
@@ -1438,6 +1616,15 @@ BasePill {
|
|||||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: BlurService.borderColor
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
|
|
||||||
QsMenuAnchor {
|
QsMenuAnchor {
|
||||||
id: submenuHydrator
|
id: submenuHydrator
|
||||||
anchor.window: menuWindow
|
anchor.window: menuWindow
|
||||||
@@ -1470,7 +1657,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 28
|
height: 28
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -1523,7 +1710,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 28
|
height: 28
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: backArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -1574,7 +1761,7 @@ BasePill {
|
|||||||
color: {
|
color: {
|
||||||
if (menuEntry?.isSeparator)
|
if (menuEntry?.isSeparator)
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
||||||
return itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0);
|
return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -17,8 +17,49 @@ Item {
|
|||||||
property real widgetHeight: 30
|
property real widgetHeight: 30
|
||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property var hyprlandOverviewLoader: null
|
property var hyprlandOverviewLoader: null
|
||||||
property var parentScreen: null
|
property var parentScreen: null
|
||||||
|
|
||||||
|
readonly property real _leftMargin: {
|
||||||
|
if (isVertical)
|
||||||
|
return 0;
|
||||||
|
root.x;
|
||||||
|
if (!root.parent)
|
||||||
|
return 0;
|
||||||
|
const gap = root.mapToItem(null, 0, 0).x;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _rightMargin: {
|
||||||
|
if (isVertical)
|
||||||
|
return 0;
|
||||||
|
root.x;
|
||||||
|
root.width;
|
||||||
|
if (!root.parent || !blurBarWindow)
|
||||||
|
return 0;
|
||||||
|
const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _topMargin: {
|
||||||
|
if (!isVertical)
|
||||||
|
return 0;
|
||||||
|
root.y;
|
||||||
|
if (!root.parent)
|
||||||
|
return 0;
|
||||||
|
const gap = root.mapToItem(null, 0, 0).y;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _bottomMargin: {
|
||||||
|
if (!isVertical)
|
||||||
|
return 0;
|
||||||
|
root.y;
|
||||||
|
root.height;
|
||||||
|
if (!root.parent || !blurBarWindow)
|
||||||
|
return 0;
|
||||||
|
const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
property int _desktopEntriesUpdateTrigger: 0
|
property int _desktopEntriesUpdateTrigger: 0
|
||||||
readonly property var sortedToplevels: {
|
readonly property var sortedToplevels: {
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||||
@@ -538,6 +579,60 @@ Item {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchToWorkspaceByModelData(data) {
|
||||||
|
if (!data)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (root.useExtWorkspace && (data.id || data.name)) {
|
||||||
|
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (CompositorService.compositor) {
|
||||||
|
case "niri":
|
||||||
|
if (data.idx !== undefined)
|
||||||
|
NiriService.switchToWorkspace(data.idx);
|
||||||
|
break;
|
||||||
|
case "hyprland":
|
||||||
|
if (data.id)
|
||||||
|
Hyprland.dispatch(`workspace ${data.id}`);
|
||||||
|
break;
|
||||||
|
case "dwl":
|
||||||
|
if (data.tag !== undefined)
|
||||||
|
DwlService.switchToTag(root.screenName, data.tag);
|
||||||
|
break;
|
||||||
|
case "sway":
|
||||||
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
|
if (data.num)
|
||||||
|
try {
|
||||||
|
I3.dispatch(`workspace number ${data.num}`);
|
||||||
|
} catch (_) {}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosestWorkspaceIndex(localX, localY) {
|
||||||
|
if (workspaceRepeater.count === 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
let closestIdx = -1;
|
||||||
|
let closestDist = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < workspaceRepeater.count; i++) {
|
||||||
|
const item = workspaceRepeater.itemAt(i);
|
||||||
|
if (!item)
|
||||||
|
continue;
|
||||||
|
const center = item.mapToItem(root, item.width / 2, item.height / 2);
|
||||||
|
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
function switchWorkspace(direction) {
|
function switchWorkspace(direction) {
|
||||||
if (useExtWorkspace) {
|
if (useExtWorkspace) {
|
||||||
const realWorkspaces = getRealWorkspaces();
|
const realWorkspaces = getRealWorkspaces();
|
||||||
@@ -751,8 +846,15 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
id: edgeMouseArea
|
||||||
acceptedButtons: Qt.RightButton
|
z: -1
|
||||||
|
x: -root._leftMargin
|
||||||
|
y: -root._topMargin
|
||||||
|
width: root.width + root._leftMargin + root._rightMargin
|
||||||
|
height: root.height + root._topMargin + root._bottomMargin
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
property real touchpadAccumulator: 0
|
property real touchpadAccumulator: 0
|
||||||
property real mouseAccumulator: 0
|
property real mouseAccumulator: 0
|
||||||
@@ -765,12 +867,20 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
if (mouse.button === Qt.RightButton) {
|
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
|
||||||
|
switch (mouse.button) {
|
||||||
|
case Qt.RightButton:
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
NiriService.toggleOverview();
|
NiriService.toggleOverview();
|
||||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case Qt.LeftButton:
|
||||||
|
const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
|
||||||
|
if (idx >= 0)
|
||||||
|
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,5 +1955,27 @@ Item {
|
|||||||
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
|
||||||
DMSService.addSubscription("extworkspace");
|
DMSService.addSubscription("extworkspace");
|
||||||
}
|
}
|
||||||
|
_updateBlurRegistration();
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool _blurRegistered: false
|
||||||
|
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
|
||||||
|
|
||||||
|
on_ShouldBlurChanged: _updateBlurRegistration()
|
||||||
|
|
||||||
|
function _updateBlurRegistration() {
|
||||||
|
if (_shouldBlur && !_blurRegistered) {
|
||||||
|
blurBarWindow.registerBlurWidget(visualBackground);
|
||||||
|
_blurRegistered = true;
|
||||||
|
} else if (!_shouldBlur && _blurRegistered) {
|
||||||
|
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
|
||||||
|
blurBarWindow.unregisterBlurWidget(visualBackground);
|
||||||
|
_blurRegistered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
|
||||||
|
blurBarWindow.unregisterBlurWidget(visualBackground);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ DankPopout {
|
|||||||
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
|
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
|
||||||
currentPlayer.pause();
|
currentPlayer.pause();
|
||||||
}
|
}
|
||||||
MprisController.activePlayer = player;
|
MprisController.setActivePlayer(player);
|
||||||
root.__hideDropdowns();
|
root.__hideDropdowns();
|
||||||
}
|
}
|
||||||
onDeviceSelected: device => {
|
onDeviceSelected: device => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -25,7 +24,7 @@ Item {
|
|||||||
}
|
}
|
||||||
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||||
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
||||||
property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
|
property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
|
||||||
property var availableDevices: {
|
property var availableDevices: {
|
||||||
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
|
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
|
||||||
return Pipewire.nodes.values.filter(node => {
|
return Pipewire.nodes.values.filter(node => {
|
||||||
@@ -336,8 +335,8 @@ Item {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData && modelData.name) {
|
||||||
Pipewire.preferredDefaultAudioSink = modelData;
|
AudioService.setDefaultSinkByName(modelData.name);
|
||||||
root.deviceSelected(modelData);
|
root.deviceSelected(modelData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Item {
|
|||||||
const id = activePlayer.identity.toLowerCase();
|
const id = activePlayer.identity.toLowerCase();
|
||||||
return id.includes("chrome") || id.includes("chromium");
|
return id.includes("chrome") || id.includes("chromium");
|
||||||
}
|
}
|
||||||
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
|
readonly property bool volumeAvailable: !!((activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio))
|
||||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||||
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
|
||||||
|
|
||||||
@@ -487,17 +487,7 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: MprisController.previousOrRewind()
|
||||||
if (!activePlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
|
||||||
activePlayer.position = 0;
|
|
||||||
} else {
|
|
||||||
activePlayer.previous();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,14 +145,7 @@ Card {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: MprisController.previousOrRewind()
|
||||||
if (!activePlayer) return
|
|
||||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
|
||||||
activePlayer.position = 0
|
|
||||||
} else {
|
|
||||||
activePlayer.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ Variants {
|
|||||||
delegate: PanelWindow {
|
delegate: PanelWindow {
|
||||||
id: dock
|
id: dock
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: dock
|
||||||
|
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||||
|
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||||
|
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
|
||||||
|
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:dock"
|
WlrLayershell.namespace: "dms:dock"
|
||||||
|
|
||||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||||
@@ -562,6 +571,15 @@ Variants {
|
|||||||
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: BlurService.borderColor
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape {
|
Shape {
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import qs.Widgets
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: root
|
||||||
|
blurX: menuContainer.x
|
||||||
|
blurY: menuContainer.y
|
||||||
|
blurWidth: root.visible ? menuContainer.width : 0
|
||||||
|
blurHeight: root.visible ? menuContainer.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:dock-context-menu"
|
WlrLayershell.namespace: "dms:dock-context-menu"
|
||||||
|
|
||||||
property var appData: null
|
property var appData: null
|
||||||
@@ -168,8 +177,8 @@ PanelWindow {
|
|||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 1
|
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||||
|
|
||||||
opacity: root.visible ? 1 : 0
|
opacity: root.visible ? 1 : 0
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pam {
|
||||||
|
id: sharedPam
|
||||||
|
lockSecured: root.shouldLock
|
||||||
|
buffer: root.sharedPasswordBuffer
|
||||||
|
onUnlockRequested: root.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
WlSessionLock {
|
WlSessionLock {
|
||||||
id: sessionLock
|
id: sessionLock
|
||||||
|
|
||||||
@@ -170,6 +177,7 @@ Scope {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: lockSurface.isActiveScreen
|
visible: lockSurface.isActiveScreen
|
||||||
lock: sessionLock
|
lock: sessionLock
|
||||||
|
pam: sharedPam
|
||||||
sharedPasswordBuffer: root.sharedPasswordBuffer
|
sharedPasswordBuffer: root.sharedPasswordBuffer
|
||||||
screenName: lockSurface.currentScreenName
|
screenName: lockSurface.currentScreenName
|
||||||
isLocked: shouldLock
|
isLocked: shouldLock
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user