1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-15 00:32:47 -04:00

Compare commits

..

25 Commits

Author SHA1 Message Date
Amaan Qureshi eb5afcdc40 flake: let module callers supply pkgs so overlays reach the build (#2244)
The nixosModule/homeModule path previously called `buildDmsPkgs pkgs` but
internally referenced `self.packages.${system}.default`, which was
instantiated via `nixpkgs.legacyPackages`, an unoverlayed pkgs. That
meant downstream flakes couldn't reach through their own overlays to
the dms-shell build (e.g. to swap `kdePackages.sonnet` or trim perl
out of the aspell closure).

Extract the derivation as `mkDmsShell = pkgs: ...` at the top-level
`let`, and call it from both `packages.${system}.dms-shell` (for
direct consumers of the flake) and `buildDmsPkgs pkgs` (for module
consumers, which now pass in the system's overlayed pkgs).

Also re-checks overrideAttrs / .override still work: `mkDmsShell pkgs`
is the same `pkgs.lib.makeOverridable` wrapper as before, just
parameterized on the caller's pkgs instance.

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>
2026-04-28 22:59:59 -04:00
Youseffo13 dd2a3f3efc Fixed mux tab having same id as locale tab (#2031)
* Feat: fix mux tab having same id as locale tab

* Feat: updated some icon

* Update KeybindsModal.qml
2026-04-28 16:34:31 -04:00
Walid Salah 161fa2dba5 Add terminal multiplexer launcher (#1687)
* Add tmux

* Add mux modal

* Restore the settings config version

* Revert typo

* Use DankModal for InputModal

* Simplify terminal flags

* use showWithOptions for inputModals instead

* Fix translation

* use Quickshell.env("TERMINAL") to choose terminal

* Fix typo

* Hide muxModal after creating new session

* Add mux check, moved exclusion to service, And use ScriptModel

* Revert unrelated change

* Add blank line
2026-04-28 16:34:29 -04:00
mihem f5f50f7add feat(running-apps): stronger active app highlight + indicator bar (#2190)
The focused app background used 20% primary opacity which was barely
visible. Increase to 45% to make the active window unambiguous at a glance.
2026-04-28 16:26:33 -04:00
bbedward 372cf2f566 core: add privesc package for privilege escalation
- Adds support for run0 and doas
fixes #998
2026-04-28 16:03:24 -04:00
bbedward b70acbc283 revert greeter clock keys 2026-04-28 15:05:55 -04:00
purian23 571c1158bf (dms): Add input group to dms setup
- Suppress fix/warnings
2026-04-28 14:34:24 -04:00
bbedward ac03a2e4b2 niri overlay: fix state binding
fixes #2301
2026-04-28 13:19:57 -04:00
Nic Ficca 07460dc3b7 Fix: close notification center after clicking action buttons (#2276)
* Close notification center after clicking action buttons

When clicking action buttons (e.g., "View", "Activate") in the
notification center, the action fires but the popout stays open. Since
the center is a layer-shell surface, it blocks focus changes on Wayland
compositors like niri, making the action appear to do nothing.

The keyboard navigation path already closes the center after invoking
actions; this brings the mouse click path in line.

Also fix closeNotificationCenter() in PopoutService to set
notificationHistoryVisible = false (matching PopoutManager._closePopout)
instead of calling close() directly, which left the visibility property
stale and caused the bell toggle to require two presses to reopen.

Fixes #2178

* Sync notificationHistoryVisible with shouldBeVisible

NotificationCenterPopout has its own notificationHistoryVisible property
that drives open/close, but the PopoutService public API (open, close,
toggle) calls DankPopout methods directly, bypassing that property. This
leaves notificationHistoryVisible stale, causing the bell toggle to
require two presses to reopen after a programmatic close.

Sync the property from onShouldBeVisibleChanged so any caller going
through open()/close() gets the state corrected automatically.
2026-04-28 11:50:40 -04:00
Kangheng Liu 7412fee590 feat(desktop): expose accept keyboard focus to desktop widgets (#2285)
Opt in by setting acceptsKeyboardFocus: true
2026-04-28 11:46:43 -04:00
bbedward 85c2954958 osd(media): workaround for firefox reporting youtube thumbnails as
players
fixes #2298
2026-04-28 11:46:26 -04:00
bbedward 7c9e805cbe port audioservice fix 2026-04-28 10:25:12 -04:00
bbedward 6926470b04 v 1.4.6 2026-04-28 10:12:56 -04:00
bbedward 713ba1efbb idle/lock: add option to turn off monitors after lock explicitly 2026-04-28 10:12:35 -04:00
bbedward 1919ca7243 fix cherry pick issues 2026-04-28 10:01:09 -04:00
Archit Arora bcf41ed5ca feat(system-tray): add icon tinting (#2266) 2026-04-25 15:32:14 -04:00
purian23 5033bdc630 (settings): Update monocrhrome & settings 2026-04-25 15:29:27 -04:00
purian23 b8bfaf9a26 (dbar): Settings reorg 2026-04-25 14:52:38 -04:00
Kangheng Liu da45714c54 keybinds: add move workspace to monitor keybinds (#2268)
and distinguish with move columns
2026-04-25 14:45:23 -04:00
bbedward 0c2d00b79c fix transparency 2026-04-25 11:54:19 -04:00
purian23 c10b42f599 dms(blur): Dank all the things 2026-04-25 11:54:18 -04:00
bbedward 4c617cf022 fix popout transparency 2026-04-25 11:54:18 -04:00
bbedward e75b95b854 blur: revise general blur styling and refine it
cherry-pick of 1abb2210 from master, translated to hotfix-1.4.5
(omits ElevationShadow gating since shadow overhaul isn't on this
branch; gates existing MultiEffect-based shadows by BlurService.enabled
instead). Drops DoNotDisturbDetail.qml hunk since the file does not
exist on this branch.
2026-04-25 11:54:18 -04:00
Lucas 6b15670918 nix: update quickshell version (#2263)
Updated the quickshell revision to 783c95, matching the "stable" package in other DMS distributions.
2026-04-24 17:17:36 -04:00
Walid Salah c52b9e19a1 Fix focused app when switching to empty workspace (#2259)
* Fix multiple screens on niri, when switching to an empty wokspace the other screen focused app widget would get confused

* Blank workspace fix
2026-04-24 17:17:36 -04:00
118 changed files with 11568 additions and 5545 deletions
+5 -11
View File
@@ -4,6 +4,7 @@ package main
import ( import (
"bufio" "bufio"
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -15,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -130,12 +132,8 @@ func updateArchLinux() error {
return errdefs.ErrUpdateCancelled return errdefs.ErrUpdateCancelled
} }
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName) fmt.Printf("\nRunning: pacman -S %s\n", packageName)
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName) if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
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) fmt.Printf("Error: Failed to update using pacman: %v\n", err)
return err return err
} }
@@ -479,11 +477,7 @@ func updateDMSBinary() error {
fmt.Printf("Installing to %s...\n", currentPath) fmt.Printf("Installing to %s...\n", currentPath)
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath) if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
replaceCmd.Stdin = os.Stdin
replaceCmd.Stdout = os.Stdout
replaceCmd.Stderr = os.Stderr
if err := replaceCmd.Run(); err != nil {
return fmt.Errorf("failed to replace binary: %w", err) return fmt.Errorf("failed to replace binary: %w", err)
} }
+26 -51
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"bufio" "bufio"
"context"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -13,6 +14,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@@ -29,7 +31,7 @@ var greeterInstallCmd = &cobra.Command{
Use: "install", Use: "install",
Short: "Install and configure DMS greeter", Short: "Install and configure DMS greeter",
Long: "Install greetd and configure it to use DMS as the greeter interface", Long: "Install greetd and configure it to use DMS as the greeter interface",
PreRunE: requireMutableSystemCommand, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -51,9 +53,10 @@ var greeterInstallCmd = &cobra.Command{
} }
var greeterSyncCmd = &cobra.Command{ var greeterSyncCmd = &cobra.Command{
Use: "sync", Use: "sync",
Short: "Sync DMS theme and settings with greeter", Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen", Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
auth, _ := cmd.Flags().GetBool("auth") auth, _ := cmd.Flags().GetBool("auth")
@@ -82,7 +85,7 @@ var greeterEnableCmd = &cobra.Command{
Use: "enable", Use: "enable",
Short: "Enable DMS greeter in greetd config", Short: "Enable DMS greeter in greetd config",
Long: "Configure greetd to use DMS as the greeter", Long: "Configure greetd to use DMS as the greeter",
PreRunE: requireMutableSystemCommand, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -118,7 +121,7 @@ var greeterUninstallCmd = &cobra.Command{
Use: "uninstall", Use: "uninstall",
Short: "Remove DMS greeter configuration and restore previous display manager", Short: "Remove DMS greeter configuration and restore previous display manager",
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state", Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
PreRunE: requireMutableSystemCommand, PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes") yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal") term, _ := cmd.Flags().GetBool("terminal")
@@ -288,10 +291,7 @@ func uninstallGreeter(nonInteractive bool) error {
} }
fmt.Println("\nDisabling greetd...") fmt.Println("\nDisabling greetd...")
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd") if err := privesc.Run(context.Background(), "", "systemctl", "disable", "greetd"); err != nil {
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err) fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
} else { } else {
fmt.Println(" ✓ greetd disabled") fmt.Println(" ✓ greetd disabled")
@@ -357,10 +357,10 @@ func restorePreDMSGreetdConfig(sudoPassword string) error {
} }
tmp.Close() tmp.Close()
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
return fmt.Errorf("failed to restore %s: %w", candidate, err) return fmt.Errorf("failed to restore %s: %w", candidate, err)
} }
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath); err != nil {
return err return err
} }
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate) fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
@@ -388,21 +388,14 @@ command = "agreety --cmd /bin/bash"
} }
tmp.Close() tmp.Close()
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, configPath); err != nil {
return fmt.Errorf("failed to write fallback greetd config: %w", err) return fmt.Errorf("failed to write fallback greetd config: %w", err)
} }
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath) _ = privesc.Run(context.Background(), sudoPassword, "chmod", "644", configPath)
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)") fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
return nil return nil
} }
func runSudoCommand(_ string, command string, args ...string) error {
cmd := exec.Command("sudo", append([]string{command}, args...)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// suggestDisplayManagerRestore scans for installed DMs and re-enables one // suggestDisplayManagerRestore scans for installed DMs and re-enables one
func suggestDisplayManagerRestore(nonInteractive bool) { func suggestDisplayManagerRestore(nonInteractive bool) {
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
@@ -421,10 +414,7 @@ func suggestDisplayManagerRestore(nonInteractive bool) {
enableDM := func(dm string) { enableDM := func(dm string) {
fmt.Printf(" Enabling %s...\n", dm) fmt.Printf(" Enabling %s...\n", dm)
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm) if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", dm); err != nil {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err) fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
} else { } else {
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm) fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
@@ -623,10 +613,7 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if response != "n" && response != "no" { if response != "n" && response != "no" {
fmt.Printf("\nAdding user to %s group...\n", greeterGroup) fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username) if err := privesc.Run(context.Background(), "", "usermod", "-aG", greeterGroup, currentUser.Username); err != nil {
addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil {
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err) return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
} }
fmt.Printf("✓ User added to %s group\n", greeterGroup) fmt.Printf("✓ User added to %s group\n", greeterGroup)
@@ -846,22 +833,19 @@ func disableDisplayManager(dmName string) (bool, error) {
actionTaken := false actionTaken := false
if state.NeedsDisable { if state.NeedsDisable {
var disableCmd *exec.Cmd var action, actionVerb string
var actionVerb string switch state.EnabledState {
case "static":
if state.EnabledState == "static" {
fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName) fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "mask", dmName) action = "mask"
actionVerb = "masked" actionVerb = "masked"
} else { default:
fmt.Printf(" Disabling %s...\n", dmName) fmt.Printf(" Disabling %s...\n", dmName)
disableCmd = exec.Command("sudo", "systemctl", "disable", dmName) action = "disable"
actionVerb = "disabled" actionVerb = "disabled"
} }
disableCmd.Stdout = os.Stdout if err := privesc.Run(context.Background(), "", "systemctl", action, dmName); err != nil {
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err) return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err)
} }
@@ -902,10 +886,7 @@ func ensureGreetdEnabled() error {
if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" {
fmt.Println(" Unmasking greetd...") fmt.Println(" Unmasking greetd...")
unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd") if err := privesc.Run(context.Background(), "", "systemctl", "unmask", "greetd"); err != nil {
unmaskCmd.Stdout = os.Stdout
unmaskCmd.Stderr = os.Stderr
if err := unmaskCmd.Run(); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err) return fmt.Errorf("failed to unmask greetd: %w", err)
} }
fmt.Println(" ✓ Unmasked greetd") fmt.Println(" ✓ Unmasked greetd")
@@ -917,10 +898,7 @@ func ensureGreetdEnabled() error {
fmt.Println(" Enabling greetd service...") fmt.Println(" Enabling greetd service...")
} }
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd") if err := privesc.Run(context.Background(), "", "systemctl", "enable", "--force", "greetd"); err != nil {
enableCmd.Stdout = os.Stdout
enableCmd.Stderr = os.Stderr
if err := enableCmd.Run(); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err) return fmt.Errorf("failed to enable greetd: %w", err)
} }
@@ -950,10 +928,7 @@ func ensureGraphicalTarget() error {
currentTargetStr := strings.TrimSpace(string(currentTarget)) currentTargetStr := strings.TrimSpace(string(currentTarget))
if currentTargetStr != "graphical.target" { if currentTargetStr != "graphical.target" {
fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr) fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr)
setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target") if err := privesc.Run(context.Background(), "", "systemctl", "set-default", "graphical.target"); err != nil {
setDefaultCmd.Stdout = os.Stdout
setDefaultCmd.Stderr = os.Stderr
if err := setDefaultCmd.Run(); err != nil {
fmt.Println("⚠ Warning: Failed to set graphical.target as default") fmt.Println("⚠ Warning: Failed to set graphical.target as default")
fmt.Println(" Greeter may not start on boot. Run manually:") fmt.Println(" Greeter may not start on boot. Run manually:")
fmt.Println(" sudo systemctl set-default graphical.target") fmt.Println(" sudo systemctl set-default graphical.target")
+36 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -11,6 +12,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -19,7 +21,7 @@ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand, PersistentPreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)
@@ -267,6 +269,8 @@ func runSetupDmsConfig(name string) error {
func runSetup() error { func runSetup() error {
fmt.Println("=== DMS Configuration Setup ===") fmt.Println("=== DMS Configuration Setup ===")
ensureInputGroup()
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd() useSystemd := promptSystemd()
@@ -340,6 +344,37 @@ func runSetup() error {
return nil return nil
} }
// Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() {
if !utils.HasGroup("input") {
return
}
currentUser := os.Getenv("USER")
if currentUser == "" {
currentUser = os.Getenv("LOGNAME")
}
if currentUser == "" {
return
}
out, err := execGroups(currentUser)
if err == nil && strings.Contains(out, "input") {
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
return
}
fmt.Println("Adding user to input group for Caps Lock OSD support...")
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
} else {
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
}
}
func execGroups(user string) (string, error) {
out, err := exec.Command("groups", user).Output()
return string(out), err
}
func promptCompositor() (deps.WindowManager, bool) { func promptCompositor() (deps.WindowManager, bool) {
fmt.Println("Select compositor:") fmt.Println("Select compositor:")
fmt.Println("1) Niri") fmt.Println("1) Niri")
+14
View File
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -269,3 +270,16 @@ func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath) return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
} }
// preRunPrivileged combines the immutable-system check with a privesc tool
// selection prompt (shown only when multiple tools are available and the
// $DMS_PRIVESC env var isn't set).
func preRunPrivileged(cmd *cobra.Command, args []string) error {
if err := requireMutableSystemCommand(cmd, args); err != nil {
return err
}
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
return err
}
return nil
}
+5 -4
View File
@@ -11,6 +11,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
func init() { func init() {
@@ -292,7 +293,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
LogOutput: "Installing base-devel development tools", LogOutput: "Installing base-devel development tools",
} }
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel") cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil { if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
return fmt.Errorf("failed to install base-devel: %w", err) return fmt.Errorf("failed to install base-devel: %w", err)
} }
@@ -463,7 +464,7 @@ func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPass
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell", CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
LogOutput: "Removing stable quickshell so quickshell-git can be installed", LogOutput: "Removing stable quickshell so quickshell-git can be installed",
} }
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell") cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil { if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
return fmt.Errorf("failed to remove stable quickshell: %w", err) return fmt.Errorf("failed to remove stable quickshell: %w", err)
} }
@@ -501,7 +502,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
@@ -779,7 +780,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
installArgs := []string{"pacman", "-U", "--noconfirm"} installArgs := []string{"pacman", "-U", "--noconfirm"}
installArgs = append(installArgs, files...) installArgs = append(installArgs, files...)
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " ")) installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
fileNames := make([]string, len(files)) fileNames := make([]string, len(files))
for i, f := range files { for i, f := range files {
+2 -22
View File
@@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/version" "github.com/AvengeMedia/DankMaterialShell/core/internal/version"
) )
@@ -55,27 +56,6 @@ func (b *BaseDistribution) logError(message string, err error) {
b.log(errorMsg) b.log(errorMsg)
} }
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
func escapeSingleQuotes(s string) string {
return strings.ReplaceAll(s, "'", "'\\''")
}
// MakeSudoCommand creates a command string that safely passes password to sudo.
// This helper escapes special characters in the password to prevent shell injection
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
func MakeSudoCommand(sudoPassword string, command string) string {
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
}
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
// The password is properly escaped to prevent shell injection and syntax errors.
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
cmdStr := MakeSudoCommand(sudoPassword, command)
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
}
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency { func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
status := deps.StatusMissing status := deps.StatusMissing
if b.commandExists(name) { if b.commandExists(name) {
@@ -710,7 +690,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
} }
// Install to /usr/local/bin // Install to /usr/local/bin
installCmd := ExecSudoCommand(ctx, sudoPassword, installCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath)) fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install DMS binary: %w", err) return fmt.Errorf("failed to install DMS binary: %w", err)
+12 -11
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
func init() { func init() {
@@ -191,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Updating APT package lists", LogOutput: "Updating APT package lists",
} }
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil { if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
return fmt.Errorf("failed to update package lists: %w", err) return fmt.Errorf("failed to update package lists: %w", err)
} }
@@ -208,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential") cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -224,7 +225,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
@@ -450,7 +451,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName) keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
// Create keyrings directory if it doesn't exist // Create keyrings directory if it doesn't exist
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings") mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
if err := mkdirCmd.Run(); err != nil { if err := mkdirCmd.Run(); err != nil {
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err)) d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
} }
@@ -464,7 +465,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath) keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd) cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil { if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
} }
@@ -480,7 +481,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile), CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
} }
addRepoCmd := ExecSudoCommand(ctx, sudoPassword, addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile)) fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err) return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
@@ -500,7 +501,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
CommandInfo: "sudo apt-get update", CommandInfo: "sudo apt-get update",
} }
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err) return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
} }
@@ -546,7 +547,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress) return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
} }
@@ -634,7 +635,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
args := []string{"apt-get", "install", "-y"} args := []string{"apt-get", "install", "-y"}
args = append(args, depList...) args = append(args, depList...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82) return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
} }
@@ -652,7 +653,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup") rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -691,7 +692,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go") installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90) return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
} }
+5 -4
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
func init() { func init() {
@@ -254,7 +255,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
args = append(args, missingPkgs...) args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
f.logError("failed to install prerequisites", err) f.logError("failed to install prerequisites", err)
@@ -437,7 +438,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL), CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL)) fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -461,7 +462,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile), CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
} }
priorityCmd := ExecSudoCommand(ctx, sudoPassword, priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile)) fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
priorityOutput, err := priorityCmd.CombinedOutput() priorityOutput, err := priorityCmd.CombinedOutput()
if err != nil { if err != nil {
@@ -537,7 +538,7 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
+15 -14
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
var GentooGlobalUseFlags = []string{ var GentooGlobalUseFlags = []string{
@@ -201,9 +202,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
var cmd *exec.Cmd var cmd *exec.Cmd
if hasUse { if hasUse {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags)) cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
} else { } else {
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags)) cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
} }
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
@@ -281,7 +282,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Syncing Portage tree with emerge --sync", LogOutput: "Syncing Portage tree with emerge --sync",
} }
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet") syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
syncOutput, syncErr := syncCmd.CombinedOutput() syncOutput, syncErr := syncCmd.CombinedOutput()
if syncErr != nil { if syncErr != nil {
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput))) g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
@@ -302,7 +303,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
args := []string{"emerge", "--ask=n", "--quiet"} args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...) args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
g.logError("failed to install prerequisites", err) g.logError("failed to install prerequisites", err)
@@ -503,14 +504,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0) return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
} }
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error { func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
packageUseDir := "/etc/portage/package.use" packageUseDir := "/etc/portage/package.use"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", packageUseDir)) fmt.Sprintf("mkdir -p %s", packageUseDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil { if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output))) g.log(fmt.Sprintf("mkdir output: %s", string(output)))
@@ -524,7 +525,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
if checkExistingCmd.Run() == nil { if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName)) g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword, replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir)) fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
if output, err := replaceCmd.CombinedOutput(); err != nil { if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output))) g.log(fmt.Sprintf("sed delete output: %s", string(output)))
@@ -532,7 +533,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
} }
} }
appendCmd := ExecSudoCommand(ctx, sudoPassword, appendCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir)) fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
output, err := appendCmd.CombinedOutput() output, err := appendCmd.CombinedOutput()
@@ -557,7 +558,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
} }
// Enable GURU repository // Enable GURU repository
enableCmd := ExecSudoCommand(ctx, sudoPassword, enableCmd := privesc.ExecCommand(ctx, sudoPassword,
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code") "eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
output, err := enableCmd.CombinedOutput() output, err := enableCmd.CombinedOutput()
@@ -589,7 +590,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
LogOutput: "Syncing GURU repository", LogOutput: "Syncing GURU repository",
} }
syncCmd := ExecSudoCommand(ctx, sudoPassword, syncCmd := privesc.ExecCommand(ctx, sudoPassword,
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code") "emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
syncOutput, syncErr := syncCmd.CombinedOutput() syncOutput, syncErr := syncCmd.CombinedOutput()
@@ -622,7 +623,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
acceptKeywordsDir := "/etc/portage/package.accept_keywords" acceptKeywordsDir := "/etc/portage/package.accept_keywords"
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir)) fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
if output, err := mkdirCmd.CombinedOutput(); err != nil { if output, err := mkdirCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("mkdir output: %s", string(output))) g.log(fmt.Sprintf("mkdir output: %s", string(output)))
@@ -636,7 +637,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
if checkExistingCmd.Run() == nil { if checkExistingCmd.Run() == nil {
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName)) g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/") escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
replaceCmd := ExecSudoCommand(ctx, sudoPassword, replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir)) fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
if output, err := replaceCmd.CombinedOutput(); err != nil { if output, err := replaceCmd.CombinedOutput(); err != nil {
g.log(fmt.Sprintf("sed delete output: %s", string(output))) g.log(fmt.Sprintf("sed delete output: %s", string(output)))
@@ -644,7 +645,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
} }
} }
appendCmd := ExecSudoCommand(ctx, sudoPassword, appendCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir)) fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
output, err := appendCmd.CombinedOutput() output, err := appendCmd.CombinedOutput()
@@ -695,6 +696,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0) return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
} }
+10 -18
View File
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
// ManualPackageInstaller provides methods for installing packages from source // ManualPackageInstaller provides methods for installing packages from source
@@ -143,7 +144,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
CommandInfo: "sudo make install", CommandInfo: "sudo make install",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
m.logError("failed to install dgop", err) m.logError("failed to install dgop", err)
@@ -213,7 +214,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
CommandInfo: "dpkg -i niri.deb", CommandInfo: "dpkg -i niri.deb",
} }
installDebCmd := ExecSudoCommand(ctx, sudoPassword, installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir)) fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
output, err := installDebCmd.CombinedOutput() output, err := installDebCmd.CombinedOutput()
@@ -324,7 +325,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
CommandInfo: "sudo cmake --install build", CommandInfo: "sudo cmake --install build",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build") installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err) return fmt.Errorf("failed to install quickshell: %w", err)
@@ -387,7 +388,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
CommandInfo: "sudo make install", CommandInfo: "sudo make install",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install") installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Hyprland: %w", err) return fmt.Errorf("failed to install Hyprland: %w", err)
@@ -453,7 +454,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/", CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, installCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir)) fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install Ghostty: %w", err) return fmt.Errorf("failed to install Ghostty: %w", err)
@@ -492,16 +493,11 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath), CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
} }
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath) if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err) return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
} }
// Make it executable if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make matugen executable: %w", err) return fmt.Errorf("failed to make matugen executable: %w", err)
} }
@@ -646,15 +642,11 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath), CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
} }
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath) if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := copyCmd.Run(); err != nil {
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err) return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
} }
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath) if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
if err := chmodCmd.Run(); err != nil {
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err) return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
} }
+8 -7
View File
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
func init() { func init() {
@@ -249,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
args := []string{"zypper", "install", "-y"} args := []string{"zypper", "install", "-y"}
args = append(args, missingPkgs...) args = append(args, missingPkgs...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
o.logError("failed to install prerequisites", err) o.logError("failed to install prerequisites", err)
@@ -485,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL), CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("zypper addrepo -f %s", repoURL)) fmt.Sprintf("zypper addrepo -f %s", repoURL))
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err)) o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
@@ -506,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh", CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
} }
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh") refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to refresh repositories: %w", err) return fmt.Errorf("failed to refresh repositories: %w", err)
} }
@@ -587,7 +588,7 @@ func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sud
} }
for _, alias := range aliases { for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias))) cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput() repoOutput, err := cmd.CombinedOutput()
if err != nil { if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput)))) o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
@@ -645,7 +646,7 @@ func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packag
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
@@ -773,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
CommandInfo: "sudo cmake --install build", CommandInfo: "sudo cmake --install build",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build") installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
installCmd.Dir = tmpDir installCmd.Dir = tmpDir
if err := installCmd.Run(); err != nil { if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install quickshell: %w", err) return fmt.Errorf("failed to install quickshell: %w", err)
@@ -797,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
CommandInfo: "sudo zypper install rustup", CommandInfo: "sudo zypper install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup") rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
+13 -12
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
) )
func init() { func init() {
@@ -177,7 +178,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Updating APT package lists", LogOutput: "Updating APT package lists",
} }
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil { if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
return fmt.Errorf("failed to update package lists: %w", err) return fmt.Errorf("failed to update package lists: %w", err)
} }
@@ -195,7 +196,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential") checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
if err := checkCmd.Run(); err != nil { if err := checkCmd.Run(); err != nil {
// Not installed, install it // Not installed, install it
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential") cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil { if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
return fmt.Errorf("failed to install build-essential: %w", err) return fmt.Errorf("failed to install build-essential: %w", err)
} }
@@ -211,7 +212,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev") "apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
@@ -398,7 +399,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool) enabledRepos := make(map[string]bool)
installPPACmd := ExecSudoCommand(ctx, sudoPassword, installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
"apt-get install -y software-properties-common") "apt-get install -y software-properties-common")
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil { if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
return fmt.Errorf("failed to install software-properties-common: %w", err) return fmt.Errorf("failed to install software-properties-common: %w", err)
@@ -416,7 +417,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL), CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, cmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL)) fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil { if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err) u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
@@ -437,7 +438,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
CommandInfo: "sudo apt-get update", CommandInfo: "sudo apt-get update",
} }
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil { if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err) return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
} }
@@ -504,7 +505,7 @@ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []st
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd) return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
@@ -591,7 +592,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
args := []string{"apt-get", "install", "-y"} args := []string{"apt-get", "install", "-y"}
args = append(args, depList...) args = append(args, depList...)
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82) return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
} }
@@ -609,7 +610,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
CommandInfo: "sudo apt-get install rustup", CommandInfo: "sudo apt-get install rustup",
} }
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup") rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil { if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
return fmt.Errorf("failed to install rustup: %w", err) return fmt.Errorf("failed to install rustup: %w", err)
} }
@@ -649,7 +650,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports", CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
} }
addPPACmd := ExecSudoCommand(ctx, sudoPassword, addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
"add-apt-repository -y ppa:longsleep/golang-backports") "add-apt-repository -y ppa:longsleep/golang-backports")
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil { if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
return fmt.Errorf("failed to add Go PPA: %w", err) return fmt.Errorf("failed to add Go PPA: %w", err)
@@ -664,7 +665,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get update", CommandInfo: "sudo apt-get update",
} }
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update") updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil { if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err) return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
} }
@@ -678,7 +679,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
CommandInfo: "sudo apt-get install golang-go", CommandInfo: "sudo apt-get install golang-go",
} }
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go") installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90) return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
} }
+84 -176
View File
@@ -16,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/sblinch/kdl-go" "github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document" "github.com/sblinch/kdl-go/document"
@@ -345,56 +346,17 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
switch config.Family { switch config.Family {
case distros.FamilyArch: case distros.FamilyArch:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm greetd")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"pacman -S --needed --noconfirm greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
}
case distros.FamilyFedora: case distros.FamilyFedora:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y greetd")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"dnf install -y greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
}
case distros.FamilySUSE: case distros.FamilySUSE:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y greetd")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, case distros.FamilyUbuntu, distros.FamilyDebian:
"zypper install -y greetd") installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
}
case distros.FamilyUbuntu:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
}
case distros.FamilyDebian:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"apt-get install -y greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
}
case distros.FamilyGentoo: case distros.FamilyGentoo:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
"emerge --ask n sys-apps/greetd")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-apps/greetd")
}
case distros.FamilyNix: case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix") return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
default: default:
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family) return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
} }
@@ -455,56 +417,56 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug)) logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug))
if _, err := exec.LookPath("gpg"); err != nil { if _, err := exec.LookPath("gpg"); err != nil {
logFunc("Installing gnupg for OBS repository key import...") logFunc("Installing gnupg for OBS repository key import...")
installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg") installGPGCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y gnupg")
installGPGCmd.Stdout = os.Stdout installGPGCmd.Stdout = os.Stdout
installGPGCmd.Stderr = os.Stderr installGPGCmd.Stderr = os.Stderr
if err := installGPGCmd.Run(); err != nil { if err := installGPGCmd.Run(); err != nil {
logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err)) logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err))
} }
} }
mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings") mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
mkdirCmd.Stdout = os.Stdout mkdirCmd.Stdout = os.Stdout
mkdirCmd.Stderr = os.Stderr mkdirCmd.Stderr = os.Stderr
mkdirCmd.Run() mkdirCmd.Run()
addKeyCmd := exec.CommandContext(ctx, "bash", "-c", addKeyCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL)) fmt.Sprintf(`bash -c "curl -fsSL %s | gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg"`, keyURL))
addKeyCmd.Stdout = os.Stdout addKeyCmd.Stdout = os.Stdout
addKeyCmd.Stderr = os.Stderr addKeyCmd.Stderr = os.Stderr
addKeyCmd.Run() addKeyCmd.Run()
addRepoCmd := exec.CommandContext(ctx, "bash", "-c", addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf(`echo '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list`, repoLine)) fmt.Sprintf(`bash -c "echo '%s' > /etc/apt/sources.list.d/danklinux.list"`, repoLine))
addRepoCmd.Stdout = os.Stdout addRepoCmd.Stdout = os.Stdout
addRepoCmd.Stderr = os.Stderr addRepoCmd.Stderr = os.Stderr
addRepoCmd.Run() addRepoCmd.Run()
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run() privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter") installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
case distros.FamilySUSE: case distros.FamilySUSE:
repoURL := getOpenSUSEOBSRepoURL(osInfo) repoURL := getOpenSUSEOBSRepoURL(osInfo)
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL) failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo zypper addrepo %s\nsudo zypper refresh && sudo zypper install dms-greeter", repoURL)
logFunc("Adding DankLinux OBS repository...") logFunc("Adding DankLinux OBS repository...")
addRepoCmd := exec.CommandContext(ctx, "sudo", "zypper", "addrepo", repoURL) addRepoCmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper addrepo %s", repoURL))
addRepoCmd.Stdout = os.Stdout addRepoCmd.Stdout = os.Stdout
addRepoCmd.Stderr = os.Stderr addRepoCmd.Stderr = os.Stderr
addRepoCmd.Run() addRepoCmd.Run()
exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run() privesc.ExecCommand(ctx, sudoPassword, "zypper refresh").Run()
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter") installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y dms-greeter")
case distros.FamilyUbuntu: case distros.FamilyUbuntu:
failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter" failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter"
logFunc("Enabling PPA ppa:avengemedia/danklinux...") logFunc("Enabling PPA ppa:avengemedia/danklinux...")
ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux") ppacmd := privesc.ExecCommand(ctx, sudoPassword, "add-apt-repository -y ppa:avengemedia/danklinux")
ppacmd.Stdout = os.Stdout ppacmd.Stdout = os.Stdout
ppacmd.Stderr = os.Stderr ppacmd.Stderr = os.Stderr
ppacmd.Run() ppacmd.Run()
exec.CommandContext(ctx, "sudo", "apt-get", "update").Run() privesc.ExecCommand(ctx, sudoPassword, "apt-get update").Run()
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "dms-greeter") installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y dms-greeter")
case distros.FamilyFedora: case distros.FamilyFedora:
failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter" failHint = "⚠ dms-greeter install failed. Enable COPR manually: sudo dnf copr enable avengemedia/danklinux && sudo dnf install dms-greeter"
logFunc("Enabling COPR avengemedia/danklinux...") logFunc("Enabling COPR avengemedia/danklinux...")
coprcmd := exec.CommandContext(ctx, "sudo", "dnf", "copr", "enable", "-y", "avengemedia/danklinux") coprcmd := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y avengemedia/danklinux")
coprcmd.Stdout = os.Stdout coprcmd.Stdout = os.Stdout
coprcmd.Stderr = os.Stderr coprcmd.Stderr = os.Stderr
coprcmd.Run() coprcmd.Run()
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "dms-greeter") installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y dms-greeter")
case distros.FamilyArch: case distros.FamilyArch:
aurHelper := "" aurHelper := ""
for _, helper := range []string{"paru", "yay"} { for _, helper := range []string{"paru", "yay"} {
@@ -557,25 +519,25 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() { if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
action = "Updated" action = "Updated"
} }
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err) return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
} }
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst)) logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "+x", wrapperDst); err != nil {
return fmt.Errorf("failed to make wrapper executable: %w", err) return fmt.Errorf("failed to make wrapper executable: %w", err)
} }
osInfo, err := distros.GetOSInfo() osInfo, err := distros.GetOSInfo()
if err == nil { if err == nil {
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) { if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
} else { } else {
logFunc("✓ Set SELinux fcontext for dms-greeter") logFunc("✓ Set SELinux fcontext for dms-greeter")
} }
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
} else { } else {
logFunc("✓ Restored SELinux context for dms-greeter") logFunc("✓ Restored SELinux context for dms-greeter")
@@ -601,7 +563,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return fmt.Errorf("failed to stat cache directory: %w", err) return fmt.Errorf("failed to stat cache directory: %w", err)
} }
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", cacheDir); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err) return fmt.Errorf("failed to create cache directory: %w", err)
} }
created = true created = true
@@ -613,17 +575,17 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
daemonUser := DetectGreeterUser() daemonUser := DetectGreeterUser()
preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group) preferredOwner := fmt.Sprintf("%s:%s", daemonUser, group)
owner := preferredOwner owner := preferredOwner
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, cacheDir); err != nil {
// Some setups may not have a matching daemon user at this moment; fall back // Some setups may not have a matching daemon user at this moment; fall back
// to root:<group> while still allowing group-writable greeter runtime access. // to root:<group> while still allowing group-writable greeter runtime access.
fallbackOwner := fmt.Sprintf("root:%s", group) fallbackOwner := fmt.Sprintf("root:%s", group)
if fallbackErr := runSudoCmd(sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil { if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", fallbackOwner, cacheDir); fallbackErr != nil {
return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr) return fmt.Errorf("failed to set cache directory owner (preferred %s: %v; fallback %s: %w)", preferredOwner, err, fallbackOwner, fallbackErr)
} }
owner = fallbackOwner owner = fallbackOwner
} }
if err := runSudoCmd(sudoPassword, "chmod", "2770", cacheDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory permissions: %w", err) return fmt.Errorf("failed to set cache directory permissions: %w", err)
} }
@@ -635,13 +597,13 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
filepath.Join(cacheDir, ".cache"), filepath.Join(cacheDir, ".cache"),
} }
for _, dir := range runtimeDirs { for _, dir := range runtimeDirs {
if err := runSudoCmd(sudoPassword, "mkdir", "-p", dir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err) return fmt.Errorf("failed to create cache runtime directory %s: %w", dir, err)
} }
if err := runSudoCmd(sudoPassword, "chown", owner, dir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, dir); err != nil {
return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err) return fmt.Errorf("failed to set owner for cache runtime directory %s: %w", dir, err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "2770", dir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err) return fmt.Errorf("failed to set permissions for cache runtime directory %s: %w", dir, err)
} }
} }
@@ -653,7 +615,7 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
} }
if isSELinuxEnforcing() && utils.CommandExists("restorecon") { if isSELinuxEnforcing() && utils.CommandExists("restorecon") {
if err := runSudoCmd(sudoPassword, "restorecon", "-Rv", cacheDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "restorecon", "-Rv", cacheDir); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context for %s: %v", cacheDir, err))
} }
} }
@@ -678,13 +640,13 @@ func ensureGreeterMemoryCompatLink(logFunc func(string), sudoPassword, legacyPat
info, err := os.Lstat(legacyPath) info, err := os.Lstat(legacyPath)
if err == nil && info.Mode().IsRegular() { if err == nil && info.Mode().IsRegular() {
if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) { if _, stateErr := os.Stat(statePath); os.IsNotExist(stateErr) {
if copyErr := runSudoCmd(sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil { if copyErr := privesc.Run(context.Background(), sudoPassword, "cp", "-f", legacyPath, statePath); copyErr != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr)) logFunc(fmt.Sprintf("⚠ Warning: Failed to migrate legacy greeter memory file to %s: %v", statePath, copyErr))
} }
} }
} }
if err := runSudoCmd(sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sfn", statePath, legacyPath); err != nil {
return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err) return fmt.Errorf("failed to create greeter memory compatibility symlink %s -> %s: %w", legacyPath, statePath, err)
} }
@@ -711,7 +673,7 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err) return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
} }
@@ -728,15 +690,15 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
} }
tmp.Close() tmp.Close()
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err) return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err) return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
} }
if utils.CommandExists("apparmor_parser") { if utils.CommandExists("apparmor_parser") {
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err)) logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest) logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
} else { } else {
@@ -783,10 +745,10 @@ func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) err
} }
tmp.Close() tmp.Close()
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to write PAM config: %w", err) return fmt.Errorf("failed to write PAM config: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set PAM config permissions: %w", err) return fmt.Errorf("failed to set PAM config permissions: %w", err)
} }
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath) logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
@@ -807,9 +769,9 @@ func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
} }
if utils.CommandExists("apparmor_parser") { if utils.CommandExists("apparmor_parser") {
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest) _ = privesc.Run(context.Background(), sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
} }
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
return fmt.Errorf("failed to remove AppArmor profile: %w", err) return fmt.Errorf("failed to remove AppArmor profile: %w", err)
} }
logFunc(" ✓ Removed DMS AppArmor profile") logFunc(" ✓ Removed DMS AppArmor profile")
@@ -839,50 +801,17 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
switch config.Family { switch config.Family {
case distros.FamilyArch: case distros.FamilyArch:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "acl")
}
case distros.FamilyFedora: case distros.FamilyFedora:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "dnf install -y acl")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "dnf install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "acl")
}
case distros.FamilySUSE: case distros.FamilySUSE:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "zypper install -y acl")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "zypper install -y acl") case distros.FamilyUbuntu, distros.FamilyDebian:
} else { installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y acl")
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl")
}
case distros.FamilyUbuntu:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
}
case distros.FamilyDebian:
if sudoPassword != "" {
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl")
}
case distros.FamilyGentoo: case distros.FamilyGentoo:
if sudoPassword != "" { installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "emerge --ask n sys-fs/acl")
} else {
installCmd = exec.CommandContext(ctx, "sudo", "emerge", "--ask", "n", "sys-fs/acl")
}
case distros.FamilyNix: case distros.FamilyNix:
return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix") return fmt.Errorf("on NixOS, please add pkgs.acl to your configuration.nix")
default: default:
return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family) return fmt.Errorf("unsupported distribution family for automatic acl installation: %s", config.Family)
} }
@@ -939,7 +868,7 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
} }
// Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora). // Group ACL covers daemon users regardless of username (e.g. greetd ≠ greeter on Fedora).
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "setfacl", "-m", fmt.Sprintf("g:%s:rX", group), dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path)) logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m g:%s:rX %s", group, dir.path))
continue continue
@@ -996,7 +925,7 @@ func RemediateStaleACLs(logFunc func(string), sudoPassword string) {
continue continue
} }
for _, user := range existingUsers { for _, user := range existingUsers {
_ = runSudoCmd(sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir) _ = privesc.Run(context.Background(), sudoPassword, "setfacl", "-x", fmt.Sprintf("u:%s", user), dir)
cleaned = true cleaned = true
} }
} }
@@ -1036,7 +965,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
// Create the group if it doesn't exist yet (e.g. before greetd package is installed). // Create the group if it doesn't exist yet (e.g. before greetd package is installed).
if !utils.HasGroup(group) { if !utils.HasGroup(group) {
if err := runSudoCmd(sudoPassword, "groupadd", "-r", group); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "groupadd", "-r", group); err != nil {
return fmt.Errorf("failed to create %s group: %w", group, err) return fmt.Errorf("failed to create %s group: %w", group, err)
} }
logFunc(fmt.Sprintf("✓ Created system group %s", group)) logFunc(fmt.Sprintf("✓ Created system group %s", group))
@@ -1047,7 +976,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
if err == nil && strings.Contains(string(groupsOutput), group) { if err == nil && strings.Contains(string(groupsOutput), group) {
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group)) logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
} else { } else {
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err) return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
} }
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group)) logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
@@ -1062,7 +991,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
if strings.Contains(string(daemonGroupsOutput), group) { if strings.Contains(string(daemonGroupsOutput), group) {
logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group)) logFunc(fmt.Sprintf("✓ Greeter daemon user %s is already in %s group", daemonUser, group))
} else { } else {
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, daemonUser); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", group, daemonUser); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err)) logFunc(fmt.Sprintf("⚠ Warning: could not add %s to %s group: %v", daemonUser, group, err))
} else { } else {
logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group)) logFunc(fmt.Sprintf("✓ Added greeter daemon user %s to %s group", daemonUser, group))
@@ -1092,12 +1021,12 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
} }
} }
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
continue continue
} }
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
continue continue
} }
@@ -1309,8 +1238,8 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
} }
target := filepath.Join(cacheDir, "colors.json") target := filepath.Join(cacheDir, "colors.json")
_ = runSudoCmd(sudoPassword, "rm", "-f", target) _ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", target)
if err := runSudoCmd(sudoPassword, "ln", "-sf", source, target); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", source, target); err != nil {
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err) return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
} }
@@ -1362,9 +1291,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
} }
} }
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target) _ = privesc.Run(context.Background(), sudoPassword, "rm", "-f", link.target)
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err) return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
} }
@@ -1406,13 +1335,13 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error { func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg") destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
if state.ResolvedGreeterWallpaperPath == "" { if state.ResolvedGreeterWallpaperPath == "" {
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
return fmt.Errorf("failed to clear override file %s: %w", destPath, err) return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
} }
logFunc("✓ Cleared greeter wallpaper override") logFunc("✓ Cleared greeter wallpaper override")
return nil return nil
} }
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "rm", "-f", destPath); err != nil {
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err) return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
} }
src := state.ResolvedGreeterWallpaperPath src := state.ResolvedGreeterWallpaperPath
@@ -1423,17 +1352,17 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
if st.IsDir() { if st.IsDir() {
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src) return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
} }
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", src, destPath); err != nil {
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err) return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
} }
greeterGroup := DetectGreeterGroup() greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser() daemonUser := DetectGreeterUser()
if err := runSudoCmd(sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chown", daemonUser+":"+greeterGroup, destPath); err != nil {
if fallbackErr := runSudoCmd(sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil { if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+greeterGroup, destPath); fallbackErr != nil {
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err) return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
} }
} }
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", destPath); err != nil {
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err) return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
} }
logFunc("✓ Synced greeter wallpaper override") logFunc("✓ Synced greeter wallpaper override")
@@ -1798,10 +1727,10 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str
if err := tmpFile.Close(); err != nil { if err := tmpFile.Close(); err != nil {
return err return err
} }
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err) return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err) return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
} }
if wantFprint || wantU2f { if wantFprint || wantU2f {
@@ -1860,13 +1789,13 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
greeterDir := "/etc/greetd/niri" greeterDir := "/etc/greetd/niri"
greeterGroup := DetectGreeterGroup() greeterGroup := DetectGreeterGroup()
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", greeterDir); err != nil {
return fmt.Errorf("failed to create greetd niri directory: %w", err) return fmt.Errorf("failed to create greetd niri directory: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err) return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "chmod", "755", greeterDir); err != nil {
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err) return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
} }
@@ -1888,7 +1817,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil { if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", dmsPath, err) return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
} }
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
return fmt.Errorf("failed to install greetd niri dms config: %w", err) return fmt.Errorf("failed to install greetd niri dms config: %w", err)
} }
@@ -1911,7 +1840,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil { if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", mainPath, err) return fmt.Errorf("failed to backup %s: %w", mainPath, err)
} }
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
return fmt.Errorf("failed to install greetd niri main config: %w", err) return fmt.Errorf("failed to install greetd niri main config: %w", err)
} }
@@ -1987,7 +1916,7 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
return fmt.Errorf("failed to close temp greetd config: %w", err) return fmt.Errorf("failed to close temp greetd config: %w", err)
} }
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to update greetd config: %w", err) return fmt.Errorf("failed to update greetd config: %w", err)
} }
@@ -2003,10 +1932,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
} }
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405")) backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "cp", path, backupPath); err != nil {
return err return err
} }
return runSudoCmd(sudoPassword, "chmod", "644", backupPath) return privesc.Run(context.Background(), sudoPassword, "chmod", "644", backupPath)
} }
func (s *niriGreeterSync) processFile(filePath string) error { func (s *niriGreeterSync) processFile(filePath string) error {
@@ -2244,11 +2173,11 @@ vt = 1
return fmt.Errorf("failed to close temp greetd config: %w", err) return fmt.Errorf("failed to close temp greetd config: %w", err)
} }
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err) return fmt.Errorf("failed to create /etc/greetd: %w", err)
} }
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to install greetd config: %w", err) return fmt.Errorf("failed to install greetd config: %w", err)
} }
@@ -2352,27 +2281,6 @@ func getOpenSUSEOBSRepoURL(osInfo *distros.OSInfo) string {
return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug) return fmt.Sprintf("%s/%s/home:AvengeMedia:danklinux.repo", base, slug)
} }
func runSudoCmd(sudoPassword string, command string, args ...string) error {
var cmd *exec.Cmd
if sudoPassword != "" {
fullArgs := append([]string{command}, args...)
quotedArgs := make([]string, len(fullArgs))
for i, arg := range fullArgs {
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}
cmdStr := strings.Join(quotedArgs, " ")
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
} else {
cmd = exec.Command("sudo", append([]string{command}, args...)...)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func checkSystemdEnabled(service string) (string, error) { func checkSystemdEnabled(service string) (string, error) {
cmd := exec.Command("systemctl", "is-enabled", service) cmd := exec.Command("systemctl", "is-enabled", service)
output, _ := cmd.Output() output, _ := cmd.Output()
@@ -2389,7 +2297,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
switch state { switch state {
case "enabled", "enabled-runtime", "static", "indirect", "alias": case "enabled", "enabled-runtime", "static", "indirect", "alias":
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "disable", dm); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else { } else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm)) logFunc(fmt.Sprintf("✓ Disabled %s", dm))
@@ -2410,13 +2318,13 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
} }
if state == "masked" || state == "masked-runtime" { if state == "masked" || state == "masked-runtime" {
logFunc(" Unmasking greetd...") logFunc(" Unmasking greetd...")
if err := runSudoCmd(sudoPassword, "systemctl", "unmask", "greetd"); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "unmask", "greetd"); err != nil {
return fmt.Errorf("failed to unmask greetd: %w", err) return fmt.Errorf("failed to unmask greetd: %w", err)
} }
logFunc(" ✓ Unmasked greetd") logFunc(" ✓ Unmasked greetd")
} }
logFunc(" Enabling greetd service (--force)...") logFunc(" Enabling greetd service (--force)...")
if err := runSudoCmd(sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "enable", "--force", "greetd"); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err) return fmt.Errorf("failed to enable greetd: %w", err)
} }
logFunc("✓ greetd enabled") logFunc("✓ greetd enabled")
@@ -2436,7 +2344,7 @@ func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
return nil return nil
} }
logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current)) logFunc(fmt.Sprintf(" Setting default target to graphical.target (was: %s)...", current))
if err := runSudoCmd(sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil { if err := privesc.Run(context.Background(), sudoPassword, "systemctl", "set-default", "graphical.target"); err != nil {
return fmt.Errorf("failed to set graphical target: %w", err) return fmt.Errorf("failed to set graphical target: %w", err)
} }
logFunc("✓ Default target set to graphical.target") logFunc("✓ Default target set to graphical.target")
+385
View File
@@ -0,0 +1,385 @@
package privesc
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"sync"
)
// Tool identifies a privilege-escalation binary.
type Tool string
const (
ToolSudo Tool = "sudo"
ToolDoas Tool = "doas"
ToolRun0 Tool = "run0"
)
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
const EnvVar = "DMS_PRIVESC"
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
var (
detectOnce sync.Once
detected Tool
detectErr error
userSelected bool
)
// Detect returns the tool that should be used for privilege escalation.
// The result is cached after the first call.
func Detect() (Tool, error) {
detectOnce.Do(func() {
detected, detectErr = detectTool()
})
return detected, detectErr
}
// ResetForTesting clears cached detection state.
func ResetForTesting() {
detectOnce = sync.Once{}
detected = ""
detectErr = nil
userSelected = false
}
// AvailableTools returns the set of supported tools that are installed on
// PATH, in detection-precedence order.
func AvailableTools() []Tool {
var out []Tool
for _, t := range detectionOrder {
if t.Available() {
out = append(out, t)
}
}
return out
}
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
// along with ok=true when the variable is set. An empty or unset variable
// returns ok=false.
func EnvOverride() (Tool, bool) {
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
if v == "" {
return "", false
}
return Tool(v), true
}
// SetTool forces the detected tool to t, bypassing autodetection. Intended
// for use after the caller has prompted the user for a selection.
func SetTool(t Tool) error {
if !t.Available() {
return fmt.Errorf("%q is not installed", t.Name())
}
detectOnce = sync.Once{}
detectOnce.Do(func() {
detected = t
detectErr = nil
})
userSelected = true
return nil
}
func detectTool() (Tool, error) {
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
case "":
// fall through to autodetect
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
t := Tool(override)
if !t.Available() {
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
}
return t, nil
default:
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
}
for _, t := range detectionOrder {
if t.Available() {
return t, nil
}
}
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
}
// Name returns the binary name.
func (t Tool) Name() string { return string(t) }
// Available reports whether this tool's binary is on PATH.
func (t Tool) Available() bool {
if t == "" {
return false
}
_, err := exec.LookPath(string(t))
return err == nil
}
// SupportsStdinPassword reports whether the tool can accept a password via
// stdin. Only sudo (-S) supports this.
func (t Tool) SupportsStdinPassword() bool {
return t == ToolSudo
}
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
// bash single-quoted string.
func EscapeSingleQuotes(s string) string {
return strings.ReplaceAll(s, "'", "'\\''")
}
// MakeCommand returns a bash command string that runs `command` with the
// detected tool. When the tool supports stdin passwords and password is
// non-empty, the password is piped in. Otherwise the tool is invoked with
// no non-interactive flag so that an interactive TTY prompt is still
// possible for CLI callers.
//
// If detection fails, the returned shell string exits 1 with an error
// message so callers that treat the *exec.Cmd as infallible still fail
// deterministically.
func MakeCommand(password, command string) string {
t, err := Detect()
if err != nil {
return failingShell(err)
}
switch t {
case ToolSudo:
if password != "" {
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
}
return fmt.Sprintf("sudo %s", command)
case ToolDoas:
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
case ToolRun0:
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
default:
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
}
}
// ExecCommand builds an exec.Cmd that runs `command` as root via the
// detected tool. Detection errors surface at Run() time as a failing
// command writing a clear error to stderr.
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
}
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
// No stdin password is supplied; callers relying on non-interactive success
// should ensure cached credentials are present (see CheckCached).
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
if len(argv) == 0 {
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
}
t, err := Detect()
if err != nil {
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
}
switch t {
case ToolSudo, ToolDoas:
return exec.CommandContext(ctx, string(t), argv...)
case ToolRun0:
return exec.CommandContext(ctx, "run0", argv...)
default:
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
}
}
func failingShell(err error) string {
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
}
// CheckCached runs a non-interactive credential probe. Returns nil if the
// tool will run commands without prompting (cached credentials, nopass, or
// polkit rule).
func CheckCached(ctx context.Context) error {
t, err := Detect()
if err != nil {
return err
}
var cmd *exec.Cmd
switch t {
case ToolSudo:
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
case ToolDoas:
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
case ToolRun0:
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
default:
return fmt.Errorf("unsupported privilege tool: %q", t)
}
return cmd.Run()
}
// ClearCache invalidates any cached credentials. No-op for tools that do
// not expose a cache-clear operation.
func ClearCache(ctx context.Context) error {
t, err := Detect()
if err != nil {
return err
}
switch t {
case ToolSudo:
return exec.CommandContext(ctx, "sudo", "-k").Run()
default:
return nil
}
}
// ValidateWithAskpass validates cached credentials using an askpass helper
// script. Only sudo supports this mechanism; the TUI uses it to trigger
// fingerprint authentication via PAM.
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
t, err := Detect()
if err != nil {
return err
}
if t != ToolSudo {
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
}
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
return cmd.Run()
}
// ValidatePassword validates the given password. Only sudo supports this
// (via `sudo -S -v`); for other tools the caller should fall back to
// CheckCached.
func ValidatePassword(ctx context.Context, password string) error {
t, err := Detect()
if err != nil {
return err
}
if t != ToolSudo {
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
}
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
stdin.Close()
_ = cmd.Wait()
return err
}
stdin.Close()
return cmd.Wait()
}
// QuoteArgsForShell wraps each argv element in single quotes so the result
// can be safely passed to bash -c.
func QuoteArgsForShell(argv []string) string {
parts := make([]string, len(argv))
for i, a := range argv {
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
}
return strings.Join(parts, " ")
}
// Run invokes argv with privilege escalation. When the tool supports stdin
// passwords and password is non-empty, the password is piped in. Otherwise
// argv is invoked directly, which may prompt on a TTY.
// Stdout and Stderr are inherited from the current process.
func Run(ctx context.Context, password string, argv ...string) error {
if len(argv) == 0 {
return fmt.Errorf("privesc.Run: argv must not be empty")
}
t, err := Detect()
if err != nil {
return err
}
var cmd *exec.Cmd
switch {
case t == ToolSudo && password != "":
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
default:
cmd = ExecArgv(ctx, argv...)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// stdinIsTTY reports whether stdin is a character device (interactive
// terminal) rather than a pipe or file.
func stdinIsTTY() bool {
fi, err := os.Stdin.Stat()
if err != nil {
return false
}
return (fi.Mode() & os.ModeCharDevice) != 0
}
// PromptCLI interactively prompts the user to pick a privilege tool when more
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
// or only one tool is available, or the env var is set, the detected tool is
// returned without any prompt.
//
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
// read from in. EOF or empty input selects the first option.
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
if userSelected {
return Detect()
}
if _, envSet := EnvOverride(); envSet {
return Detect()
}
tools := AvailableTools()
switch len(tools) {
case 0:
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
case 1:
if err := SetTool(tools[0]); err != nil {
return "", err
}
return tools[0], nil
}
if !stdinIsTTY() {
return Detect()
}
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
for i, t := range tools {
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
}
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
reader := bufio.NewReader(in)
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return "", fmt.Errorf("failed to read selection: %w", err)
}
line = strings.TrimSpace(line)
idx := 1
if line != "" {
n, convErr := strconv.Atoi(line)
if convErr != nil || n < 1 || n > len(tools) {
return "", fmt.Errorf("invalid selection %q", line)
}
idx = n
}
chosen := tools[idx-1]
if err := SetTool(chosen); err != nil {
return "", err
}
return chosen, nil
}
+1 -1
View File
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
func InitializeManager() (*Manager, error) { func InitializeManager() (*Manager, error) {
if os.Getuid() != 0 && !hasInputGroupAccess() { if os.Getuid() != 0 && !hasInputGroupAccess() {
return nil, fmt.Errorf("insufficient permissions to access input devices") return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
} }
return NewManager() return NewManager()
@@ -104,7 +104,7 @@ func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface
return false return false
} }
if reply != dbus.RequestNameReplyPrimaryOwner { if reply != dbus.RequestNameReplyPrimaryOwner {
log.Warnf("Screensaver name %s already owned by another process", name) log.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", name)
return false return false
} }
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil { if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
+8
View File
@@ -3,6 +3,7 @@ package tui
import ( import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -42,6 +43,9 @@ type Model struct {
sudoPassword string sudoPassword string
existingConfigs []ExistingConfigInfo existingConfigs []ExistingConfigInfo
fingerprintFailed bool fingerprintFailed bool
availablePrivesc []privesc.Tool
selectedPrivesc int
} }
func NewModel(version string, logFilePath string) Model { func NewModel(version string, logFilePath string) Model {
@@ -147,6 +151,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateGentooUseFlagsState(msg) return m.updateGentooUseFlagsState(msg)
case StateGentooGCCCheck: case StateGentooGCCCheck:
return m.updateGentooGCCCheckState(msg) return m.updateGentooGCCCheckState(msg)
case StateSelectPrivesc:
return m.updateSelectPrivescState(msg)
case StateAuthMethodChoice: case StateAuthMethodChoice:
return m.updateAuthMethodChoiceState(msg) return m.updateAuthMethodChoiceState(msg)
case StateFingerprintAuth: case StateFingerprintAuth:
@@ -189,6 +195,8 @@ func (m Model) View() string {
return m.viewGentooUseFlags() return m.viewGentooUseFlags()
case StateGentooGCCCheck: case StateGentooGCCCheck:
return m.viewGentooGCCCheck() return m.viewGentooGCCCheck()
case StateSelectPrivesc:
return m.viewSelectPrivesc()
case StateAuthMethodChoice: case StateAuthMethodChoice:
return m.viewAuthMethodChoice() return m.viewAuthMethodChoice()
case StateFingerprintAuth: case StateFingerprintAuth:
+1
View File
@@ -10,6 +10,7 @@ const (
StateDependencyReview StateDependencyReview
StateGentooUseFlags StateGentooUseFlags
StateGentooGCCCheck StateGentooGCCCheck
StateSelectPrivesc
StateAuthMethodChoice StateAuthMethodChoice
StateFingerprintAuth StateFingerprintAuth
StatePasswordPrompt StatePasswordPrompt
+1 -10
View File
@@ -180,16 +180,7 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
} }
// Check if fingerprint is enabled return m.enterAuthPhase()
if checkFingerprintEnabled() {
m.state = StateAuthMethodChoice
m.selectedConfig = 0 // Default to fingerprint
return m, nil
} else {
m.state = StatePasswordPrompt
m.passwordInput.Focus()
return m, nil
}
case "esc": case "esc":
m.state = StateSelectWindowManager m.state = StateSelectWindowManager
return m, nil return m, nil
+2 -16
View File
@@ -56,14 +56,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = StateGentooGCCCheck m.state = StateGentooGCCCheck
return m, nil return m, nil
} }
if checkFingerprintEnabled() { return m.enterAuthPhase()
m.state = StateAuthMethodChoice
m.selectedConfig = 0
} else {
m.state = StatePasswordPrompt
m.passwordInput.Focus()
}
return m, nil
} }
if keyMsg, ok := msg.(tea.KeyMsg); ok { if keyMsg, ok := msg.(tea.KeyMsg); ok {
@@ -75,14 +68,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.selectedWM == 1 { if m.selectedWM == 1 {
return m, m.checkGCCVersion() return m, m.checkGCCVersion()
} }
if checkFingerprintEnabled() { return m.enterAuthPhase()
m.state = StateAuthMethodChoice
m.selectedConfig = 0
} else {
m.state = StatePasswordPrompt
m.passwordInput.Focus()
}
return m, nil
case "esc": case "esc":
m.state = StateDependencyReview m.state = StateDependencyReview
return m, nil return m, nil
+4 -33
View File
@@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -274,8 +275,7 @@ func (m Model) delayThenReturn() tea.Cmd {
func (m Model) tryFingerprint() tea.Cmd { func (m Model) tryFingerprint() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
clearCmd := exec.Command("sudo", "-k") _ = privesc.ClearCache(context.Background())
clearCmd.Run()
tmpDir := os.TempDir() tmpDir := os.TempDir()
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano())) askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
@@ -289,15 +289,9 @@ func (m Model) tryFingerprint() tea.Cmd {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v") if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
err := cmd.Run()
if err != nil {
return passwordValidMsg{password: "", valid: false} return passwordValidMsg{password: "", valid: false}
} }
return passwordValidMsg{password: "", valid: true} return passwordValidMsg{password: "", valid: true}
} }
} }
@@ -307,32 +301,9 @@ func (m Model) validatePassword(password string) tea.Cmd {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v") if err := privesc.ValidatePassword(ctx, password); err != nil {
stdin, err := cmd.StdinPipe()
if err != nil {
return passwordValidMsg{password: "", valid: false} return passwordValidMsg{password: "", valid: false}
} }
if err := cmd.Start(); err != nil {
return passwordValidMsg{password: "", valid: false}
}
_, err = fmt.Fprintf(stdin, "%s\n", password)
stdin.Close()
if err != nil {
return passwordValidMsg{password: "", valid: false}
}
err = cmd.Wait()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: password, valid: true} return passwordValidMsg{password: password, valid: true}
} }
} }
+133
View File
@@ -0,0 +1,133 @@
package tui
import (
"fmt"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
tea "github.com/charmbracelet/bubbletea"
)
func (m Model) viewSelectPrivesc() string {
var b strings.Builder
b.WriteString(m.renderBanner())
b.WriteString("\n")
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
b.WriteString("\n\n")
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
b.WriteString("\n\n")
for i, t := range m.availablePrivesc {
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
switch i {
case m.selectedPrivesc:
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
default:
b.WriteString(m.styles.Normal.Render(" " + label))
}
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
b.WriteString("\n\n")
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
return b.String()
}
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return m, m.listenForLogs()
}
switch keyMsg.String() {
case "up":
if m.selectedPrivesc > 0 {
m.selectedPrivesc--
}
case "down":
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
m.selectedPrivesc++
}
case "enter":
chosen := m.availablePrivesc[m.selectedPrivesc]
if err := privesc.SetTool(chosen); err != nil {
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
m.state = StateError
return m, nil
}
return m.routeToAuthAfterPrivesc()
case "esc":
m.state = StateDependencyReview
return m, nil
}
return m, nil
}
func privescToolDescription(t privesc.Tool) string {
switch t {
case privesc.ToolSudo:
return "classic sudo (supports password prompt in this installer)"
case privesc.ToolDoas:
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
case privesc.ToolRun0:
return "systemd run0 (authenticated via polkit)"
default:
return string(t)
}
}
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
// right auth flow. Sudo goes through the fingerprint/password path; doas and
// run0 skip password entry and proceed to install.
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
tool, err := privesc.Detect()
if err != nil {
m.err = err
m.state = StateError
return m, nil
}
if tool == privesc.ToolSudo {
if checkFingerprintEnabled() {
m.state = StateAuthMethodChoice
m.selectedConfig = 0
return m, nil
}
m.state = StatePasswordPrompt
m.passwordInput.Focus()
return m, nil
}
m.sudoPassword = ""
m.packageProgress = packageInstallProgressMsg{}
m.state = StateInstallingPackages
m.isLoading = true
return m, tea.Batch(m.spinner.Tick, m.installPackages())
}
// enterAuthPhase is called when dependency review (or the Gentoo screens)
// finish. It either routes directly to the sudo/fingerprint flow or shows
// the privesc-tool selection screen when multiple tools are available and
// no $DMS_PRIVESC override is set.
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
tools := privesc.AvailableTools()
_, envSet := privesc.EnvOverride()
if len(tools) == 0 {
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
m.state = StateError
return m, nil
}
if envSet || len(tools) == 1 {
return m.routeToAuthAfterPrivesc()
}
m.availablePrivesc = tools
m.selectedPrivesc = 0
m.state = StateSelectPrivesc
return m, nil
}
Generated
+5 -5
View File
@@ -23,16 +23,16 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1766725085, "lastModified": 1776854048,
"narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=", "narHash": "sha256-lLbV66V3RMNp1l8/UelmR4YzoJ5ONtgvEtiUMJATH/o=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff", "rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
"revCount": 715, "revCount": 806,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell" "url": "https://git.outfoxxed.me/quickshell/quickshell"
}, },
"original": { "original": {
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff", "rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell" "url": "https://git.outfoxxed.me/quickshell/quickshell"
} }
+86 -73
View File
@@ -4,7 +4,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
quickshell = { quickshell = {
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=41828c4180fb921df7992a5405f5ff05d2ac2fff"; url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=783c953987dc56ff0601abe6845ed96f1d00495a";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
@@ -41,10 +41,11 @@
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] ( nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
system: fn system nixpkgs.legacyPackages.${system} system: fn system nixpkgs.legacyPackages.${system}
); );
buildDmsPkgs = pkgs: { forEachLinuxSystem =
dms-shell = self.packages.${pkgs.stdenv.hostPlatform.system}.default; fn:
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default; nixpkgs.lib.genAttrs [ "aarch64-linux" "x86_64-linux" ] (
}; system: fn system nixpkgs.legacyPackages.${system}
);
mkModuleWithDmsPkgs = mkModuleWithDmsPkgs =
modulePath: modulePath:
args@{ pkgs, ... }: args@{ pkgs, ... }:
@@ -53,6 +54,7 @@
(import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; })) (import modulePath (args // { dmsPkgs = buildDmsPkgs pkgs; }))
]; ];
}; };
mkQmlImportPath = mkQmlImportPath =
pkgs: qmlPkgs: pkgs: qmlPkgs:
pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs); pkgs.lib.concatStringsSep ":" (map (o: "${o}/${pkgs.qt6.qtbase.qtQmlPrefix}") qmlPkgs);
@@ -69,10 +71,11 @@
qtimageformats qtimageformats
kimageformats kimageformats
]; ];
in
{ # Allows downstream modules to provide their own 'pkgs' (with overlays)
packages = forEachSystem ( # instead of being forced to use the flake's locked nixpkgs.
system: pkgs: mkDmsShell =
pkgs:
let let
mkDate = mkDate =
longDate: longDate:
@@ -90,86 +93,96 @@
in in
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ pkgs.lib.makeOverridable (
dms-shell = pkgs.lib.makeOverridable ( {
extraQtPackages ? [ ],
}:
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{ {
extraQtPackages ? [ ], inherit version;
}: pname = "dms-shell";
(pkgs.buildGoModule.override { go = goForPkgs pkgs; }) ( src = ./core;
let vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
--bash <($out/bin/dms completion bash) \ --replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so
--fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh)
'';
meta = { installShellCompletion --cmd dms \
description = "Desktop shell for wayland compositors built with Quickshell & GO"; --bash <($out/bin/dms completion bash) \
homepage = "https://danklinux.com"; --fish <($out/bin/dms completion fish) \
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; --zsh <($out/bin/dms completion zsh)
license = pkgs.lib.licenses.mit; '';
mainProgram = "dms";
platforms = pkgs.lib.platforms.linux;
};
}
)
) { };
meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit;
mainProgram = "dms";
platforms = pkgs.lib.platforms.linux;
};
}
)
) { };
buildDmsPkgs = pkgs: {
dms-shell = mkDmsShell pkgs;
quickshell = quickshell.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
in
{
packages = forEachSystem (
system: pkgs: {
dms-shell = mkDmsShell pkgs;
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
default = self.packages.${system}.dms-shell; default = self.packages.${system}.dms-shell;
} }
); );
+4
View File
@@ -115,4 +115,8 @@ Singleton {
return translations[context][term]; return translations[context][term];
return term; return term;
} }
function locale() {
return Qt.locale();
}
} }
+10 -4
View File
@@ -158,10 +158,16 @@ const NIRI_ACTIONS = {
{ id: "focus-monitor-right", label: "Focus Monitor Right" }, { id: "focus-monitor-right", label: "Focus Monitor Right" },
{ id: "focus-monitor-down", label: "Focus Monitor Down" }, { id: "focus-monitor-down", label: "Focus Monitor Down" },
{ id: "focus-monitor-up", label: "Focus Monitor Up" }, { id: "focus-monitor-up", label: "Focus Monitor Up" },
{ id: "move-column-to-monitor-left", label: "Move to Monitor Left" }, { id: "move-column-to-monitor-left", label: "Move Column to Monitor Left" },
{ id: "move-column-to-monitor-right", label: "Move to Monitor Right" }, { id: "move-column-to-monitor-right", label: "Move Column to Monitor Right" },
{ id: "move-column-to-monitor-down", label: "Move to Monitor Down" }, { id: "move-column-to-monitor-down", label: "Move Column to Monitor Down" },
{ id: "move-column-to-monitor-up", label: "Move to Monitor Up" } { id: "move-column-to-monitor-up", label: "Move Column to Monitor Up" },
{ id: "move-workspace-to-monitor-left", label: "Move Workspace to Monitor Left" },
{ id: "move-workspace-to-monitor-right", label: "Move Workspace to Monitor Right" },
{ id: "move-workspace-to-monitor-down", label: "Move Workspace to Monitor Down" },
{ id: "move-workspace-to-monitor-up", label: "Move Workspace to Monitor Up" },
{ id: "move-workspace-to-monitor-next", label: "Move Workspace to Next Monitor" },
{ id: "move-workspace-to-monitor-previous", label: "Move Workspace to Previous Monitor" }
], ],
"Screenshot": [ "Screenshot": [
{ id: "screenshot", label: "Screenshot (Interactive)" }, { id: "screenshot", label: "Screenshot (Interactive)" },
+14 -10
View File
@@ -167,11 +167,15 @@ Singleton {
onEnableRippleEffectsChanged: saveSettings() onEnableRippleEffectsChanged: saveSettings()
property bool blurEnabled: false property bool blurEnabled: false
onBlurEnabledChanged: saveSettings() onBlurEnabledChanged: saveSettings()
property bool blurForegroundLayers: true
onBlurForegroundLayersChanged: saveSettings()
property real blurLayerOutlineOpacity: 0.12
onBlurLayerOutlineOpacityChanged: saveSettings()
property string blurBorderColor: "outline" property string blurBorderColor: "outline"
onBlurBorderColorChanged: saveSettings() onBlurBorderColorChanged: saveSettings()
property string blurBorderCustomColor: "#ffffff" property string blurBorderCustomColor: "#ffffff"
onBlurBorderCustomColorChanged: saveSettings() onBlurBorderCustomColorChanged: saveSettings()
property real blurBorderOpacity: 1.0 property real blurBorderOpacity: 0.35
onBlurBorderOpacityChanged: saveSettings() onBlurBorderOpacityChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
@@ -190,6 +194,9 @@ Singleton {
property int selectedGpuIndex: 0 property int selectedGpuIndex: 0
property var enabledGpuPciIds: [] property var enabledGpuPciIds: []
property bool showSystemTray: true property bool showSystemTray: true
property string systemTrayIconTintMode: "none"
property int systemTrayIconTintSaturation: 50
property int systemTrayIconTintStrength: 135
property bool showClock: true property bool showClock: true
property bool showNotificationButton: true property bool showNotificationButton: true
property bool showBattery: true property bool showBattery: true
@@ -307,12 +314,6 @@ Singleton {
property bool greeterEnableFprint: false property bool greeterEnableFprint: false
property bool greeterEnableU2f: false property bool greeterEnableU2f: false
property string greeterWallpaperPath: "" property string greeterWallpaperPath: ""
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property int mediaSize: 1 property int mediaSize: 1
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
@@ -441,6 +442,11 @@ Singleton {
property bool syncModeWithPortal: true property bool syncModeWithPortal: true
property bool terminalsAlwaysDark: false property bool terminalsAlwaysDark: false
property string muxType: "tmux"
property bool muxUseCustomCommand: false
property string muxCustomCommand: ""
property string muxSessionFilter: ""
property bool runDmsMatugenTemplates: true property bool runDmsMatugenTemplates: true
property bool matugenTemplateGtk: true property bool matugenTemplateGtk: true
property bool matugenTemplateNiri: true property bool matugenTemplateNiri: true
@@ -1292,9 +1298,7 @@ Singleton {
return true; return true;
const msg = String(error || "").toLowerCase(); const msg = String(error || "").toLowerCase();
return msg.indexOf("file does not exist") !== -1 return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1;
|| msg.indexOf("no such file") !== -1
|| msg.indexOf("enoent") !== -1;
} }
function loadPluginSettings() { function loadPluginSettings() {
+23 -6
View File
@@ -561,8 +561,8 @@ Singleton {
property color success: currentThemeData.success || "#4CAF50" property color success: currentThemeData.success || "#4CAF50"
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12) property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08) property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.12 : 0.08)
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16) property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.24 : 0.16)
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3) property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04) property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
@@ -571,17 +571,28 @@ Singleton {
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08) property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12) property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15) property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1) property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, transparentBlurLayers ? 0.3 : 0.1)
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2) property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
readonly property bool blurForegroundLayers: BlurService.enabled && (typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true))
readonly property bool transparentBlurLayers: BlurService.enabled && !blurForegroundLayers
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
readonly property color floatingSurface: transparentBlurLayers ? "transparent" : readableSurface
readonly property color floatingSurfaceHigh: transparentBlurLayers ? "transparent" : readableSurfaceHigh
readonly property color nestedSurface: floatingSurfaceHigh
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
readonly property real layerOutlineOpacity: BlurService.enabled ? blurLayerOutlineOpacity : 0.08
readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08) property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3) property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06) property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7) property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5) property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05) property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 0.625) : 0.05)
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08) property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, layerOutlineOpacity)
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12) property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 1.5) : 0.12)
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12) property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16) property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
@@ -599,6 +610,12 @@ Singleton {
} }
} }
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (blurForegroundLayers ? withAlpha(surfaceContainerHigh, Math.min(popupTransparency, 0.24)) : withAlpha(surfaceContainer, popupTransparency))
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
readonly property real ccSliderTrackOpacity: transparentBlurLayers ? 0.18 : popupTransparency
readonly property color ccTileActiveText: { readonly property color ccTileActiveText: {
switch (SettingsData.controlCenterTileColorMode) { switch (SettingsData.controlCenterTileColorMode) {
case "primaryContainer": case "primaryContainer":
+11 -7
View File
@@ -47,9 +47,11 @@ var SPEC = {
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
blurEnabled: { def: false }, blurEnabled: { def: false },
blurForegroundLayers: { def: true },
blurLayerOutlineOpacity: { def: 0.12, coerce: percentToUnit },
blurBorderColor: { def: "outline" }, blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" }, blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 1.0, coerce: percentToUnit }, blurBorderOpacity: { def: 0.35, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -67,6 +69,9 @@ var SPEC = {
selectedGpuIndex: { def: 0 }, selectedGpuIndex: { def: 0 },
enabledGpuPciIds: { def: [] }, enabledGpuPciIds: { def: [] },
showSystemTray: { def: true }, showSystemTray: { def: true },
systemTrayIconTintMode: { def: "none" },
systemTrayIconTintSaturation: { def: 50 },
systemTrayIconTintStrength: { def: 135 },
showClock: { def: true }, showClock: { def: true },
showNotificationButton: { def: true }, showNotificationButton: { def: true },
showBattery: { def: true }, showBattery: { def: true },
@@ -163,12 +168,6 @@ var SPEC = {
greeterEnableFprint: { def: false }, greeterEnableFprint: { def: false },
greeterEnableU2f: { def: false }, greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" }, greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
@@ -261,6 +260,11 @@ var SPEC = {
syncModeWithPortal: { def: true }, syncModeWithPortal: { def: true },
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" }, terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
muxType: { def: "tmux" },
muxUseCustomCommand: { def: false },
muxCustomCommand: { def: "" },
muxSessionFilter: { def: "" },
runDmsMatugenTemplates: { def: true }, runDmsMatugenTemplates: { def: true },
matugenTemplateGtk: { def: true }, matugenTemplateGtk: { def: true },
matugenTemplateNiri: { def: true }, matugenTemplateNiri: { def: true },
+5 -6
View File
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
import qs.Modals
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.DankDash import qs.Modules.DankDash
@@ -638,6 +639,10 @@ Item {
} }
} }
MuxModal {
id: muxModal
}
ClipboardHistoryModal { ClipboardHistoryModal {
id: clipboardHistoryModalPopup id: clipboardHistoryModalPopup
@@ -1018,12 +1023,6 @@ Item {
} }
} }
Loader {
id: powerProfileWatcherLoader
active: SettingsData.osdPowerProfileEnabled
source: "Services/PowerProfileWatcher.qml"
}
LazyLoader { LazyLoader {
id: hyprlandOverviewLoader id: hyprlandOverviewLoader
active: CompositorService.isHyprland active: CompositorService.isHyprland
+312
View File
@@ -0,0 +1,312 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:input-modal"
keepPopoutsOpen: true
property string inputTitle: ""
property string inputMessage: ""
property string inputPlaceholder: ""
property string inputText: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function (text) {}
property var onCancel: function () {}
property int selectedButton: -1
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
inputTitle = title || "";
inputMessage = message || "";
inputPlaceholder = "";
inputText = "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || ((text) => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
inputTitle = options.title || "";
inputMessage = options.message || "";
inputPlaceholder = options.placeholder || "";
inputText = options.initialText || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || ((text) => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function confirmAndClose() {
const text = inputText;
close();
if (onConfirm) {
onConfirm(text);
}
}
function cancelAndClose() {
close();
if (onCancel) {
onCancel();
}
}
function selectButton() {
if (selectedButton === 0) {
cancelAndClose();
} else {
confirmAndClose();
}
}
shouldBeVisible: false
allowStacking: true
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: cancelAndClose()
onOpened: {
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.textInputRef) {
contentLoader.item.textInputRef.forceActiveFocus();
}
});
}
content: Component {
FocusScope {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight
focus: true
property alias textInputRef: textInput
Keys.onPressed: function (event) {
const textFieldFocused = textInput.activeFocus;
switch (event.key) {
case Qt.Key_Escape:
root.cancelAndClose();
event.accepted = true;
break;
case Qt.Key_Tab:
if (textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
textInput.focus = false;
} else {
root.keyboardNavigation = true;
if (root.selectedButton === -1) {
root.selectedButton = 0;
} else if (root.selectedButton === 0) {
root.selectedButton = 1;
} else {
root.selectedButton = -1;
textInput.forceActiveFocus();
}
}
event.accepted = true;
break;
case Qt.Key_Left:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_Right:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.selectedButton !== -1) {
root.selectButton();
} else {
root.confirmAndClose();
}
event.accepted = true;
break;
}
}
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
spacing: 0
StyledText {
text: root.inputTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Item {
width: 1
height: Theme.spacingL
}
StyledText {
text: root.inputMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
visible: root.inputMessage !== ""
}
Item {
width: 1
height: root.inputMessage !== "" ? Theme.spacingL : 0
visible: root.inputMessage !== ""
}
Rectangle {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: textInput.activeFocus ? Theme.primary : "transparent"
border.width: textInput.activeFocus ? 1 : 0
TextInput {
id: textInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
selectionColor: Theme.primary
selectedTextColor: Theme.primaryText
clip: true
text: root.inputText
onTextChanged: root.inputText = text
StyledText {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
text: root.inputPlaceholder
visible: textInput.text === "" && !textInput.activeFocus
}
}
}
Item {
width: 1
height: Theme.spacingL * 1.5
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
StyledText {
text: root.cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.cancelAndClose()
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = root.confirmButtonColor;
if (root.keyboardNavigation && root.selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
StyledText {
text: root.confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.confirmAndClose()
}
}
}
Item {
width: 1
height: Theme.spacingL
}
}
}
}
}
@@ -372,7 +372,7 @@ Popup {
anchors.fill: parent anchors.fill: parent
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.floatingSurface
radius: Theme.cornerRadius radius: Theme.cornerRadius
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: 1 border.width: 1
+109 -98
View File
@@ -58,9 +58,9 @@ Item {
item: items[i], item: items[i],
flatIndex: flatIdx, flatIndex: flatIdx,
sectionId: sectionId, sectionId: sectionId,
height: 52 height: 56
}); });
cumY += 52; cumY += 56;
} }
} else { } else {
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns; var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
@@ -190,124 +190,135 @@ Item {
} }
} }
DankListView { Item {
id: mainListView id: listClip
anchors.fill: parent anchors.fill: parent
anchors.topMargin: BlurService.enabled && stickyHeader.visible ? 32 : 0
clip: true clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
model: ScriptModel { DankListView {
values: root._visualRows id: mainListView
objectProp: "_rowId" y: -listClip.anchors.topMargin
} width: parent.width
height: parent.height + listClip.anchors.topMargin
clip: true
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
add: null model: ScriptModel {
remove: null values: root._visualRows
displaced: null objectProp: "_rowId"
move: null
delegate: Item {
id: delegateRoot
required property var modelData
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
} }
ResultItem { add: null
anchors.fill: parent remove: null
visible: delegateRoot.modelData?.type === "list_item" displaced: null
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null move: null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
onClicked: { delegate: Item {
if (root.controller && delegateRoot.modelData?.item) { id: delegateRoot
root.controller.executeItem(delegateRoot.modelData.item); required property var modelData
required property int index
width: mainListView.width
height: modelData?.height ?? 52
SectionHeader {
anchors.fill: parent
visible: delegateRoot.modelData?.type === "header"
section: delegateRoot.modelData?.section ?? null
controller: root.controller
viewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
}
canChangeViewMode: {
var vt = root.controller?.viewModeVersion ?? 0;
void (vt);
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
}
ResultItem {
anchors.fill: parent
anchors.topMargin: 2
anchors.bottomMargin: 2
visible: delegateRoot.modelData?.type === "list_item"
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
onClicked: {
if (root.controller && delegateRoot.modelData?.item) {
root.controller.executeItem(delegateRoot.modelData.item);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
} }
} }
onRightClicked: (mouseX, mouseY) => { Row {
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY); id: gridRowContent
} anchors.fill: parent
} visible: delegateRoot.modelData?.type === "grid_row"
Row { Repeater {
id: gridRowContent model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
anchors.fill: parent
visible: delegateRoot.modelData?.type === "grid_row"
Repeater { Item {
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : [] id: gridCellDelegate
required property var modelData
required property int index
Item { readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
id: gridCellDelegate
required property var modelData
required property int index
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns)) width: cellWidth
height: delegateRoot.height
width: cellWidth GridItem {
height: delegateRoot.height width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "grid"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
GridItem { onClicked: {
width: parent.width - 4 if (root.controller && gridCellDelegate.modelData?.item) {
height: parent.height - 4 root.controller.executeItem(gridCellDelegate.modelData.item);
anchors.centerIn: parent }
visible: delegateRoot.modelData?.viewMode === "grid" }
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: { onRightClicked: (mouseX, mouseY) => {
if (root.controller && gridCellDelegate.modelData?.item) { root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
root.controller.executeItem(gridCellDelegate.modelData.item);
} }
} }
onRightClicked: (mouseX, mouseY) => { TileItem {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); width: parent.width - 4
} height: parent.height - 4
} anchors.centerIn: parent
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
TileItem { onClicked: {
width: parent.width - 4 if (root.controller && gridCellDelegate.modelData?.item) {
height: parent.height - 4 root.controller.executeItem(gridCellDelegate.modelData.item);
anchors.centerIn: parent }
visible: delegateRoot.modelData?.viewMode === "tile"
item: gridCellDelegate.modelData?.item ?? null
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
onClicked: {
if (root.controller && gridCellDelegate.modelData?.item) {
root.controller.executeItem(gridCellDelegate.modelData.item);
} }
}
onRightClicked: (mouseX, mouseY) => { onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
}
} }
} }
} }
@@ -365,7 +376,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
height: 32 height: 32
z: 101 z: 101
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.floatingSurface
visible: stickyHeaderSection !== null visible: stickyHeaderSection !== null
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0 readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
+1 -1
View File
@@ -50,7 +50,7 @@ Item {
id: listComponent id: listComponent
Column { Column {
spacing: 2 spacing: 4
width: contentLoader.width width: contentLoader.width
Repeater { Repeater {
+5 -2
View File
@@ -81,7 +81,7 @@ DankModal {
StyledText { StyledText {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: KeybindsService.cheatsheet.title || "Keybinds" text: KeybindsService.cheatsheet.title || i18n("Keybinds")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.primary color: Theme.primary
@@ -309,10 +309,12 @@ DankModal {
id: keyText id: keyText
anchors.centerIn: parent anchors.centerIn: parent
color: Theme.secondary color: Theme.secondary
text: modelData.key || "" text: (modelData.key || "").replace(/\+/g, " + ")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
isMonospace: true isMonospace: true
elide: Text.ElideRight
width: Math.min(implicitWidth, 148)
} }
} }
@@ -325,6 +327,7 @@ DankModal {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
opacity: 0.9 opacity: 0.9
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.NoWrap
} }
} }
} }
+621
View File
@@ -0,0 +1,621 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: muxModal
layerNamespace: "dms:mux"
property int selectedIndex: -1
property string searchText: ""
property var filteredSessions: []
function updateFilteredSessions() {
var filtered = []
var lowerSearch = searchText.trim().toLowerCase()
for (var i = 0; i < MuxService.sessions.length; i++) {
var session = MuxService.sessions[i]
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
continue
filtered.push(session)
}
filteredSessions = filtered
if (selectedIndex >= filteredSessions.length) {
selectedIndex = Math.max(0, filteredSessions.length - 1)
}
}
onSearchTextChanged: updateFilteredSessions()
Connections {
target: MuxService
function onSessionsChanged() {
updateFilteredSessions()
}
}
HyprlandFocusGrab {
id: grab
windows: [muxModal.contentWindow]
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
selectedIndex = -1
searchText = ""
MuxService.refreshSessions()
shouldHaveFocus = true
Qt.callLater(() => {
if (muxPanel && muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
})
}
function hide() {
close()
selectedIndex = -1
searchText = ""
}
function attachToSession(name) {
MuxService.attachToSession(name)
hide()
}
function renameSession(name) {
inputModal.showWithOptions({
title: I18n.tr("Rename Session"),
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
initialText: name,
onConfirm: function (newName) {
MuxService.renameSession(name, newName)
}
})
}
function killSession(name) {
confirmModal.showWithOptions({
title: I18n.tr("Kill Session"),
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
confirmText: I18n.tr("Kill"),
confirmColor: Theme.primary,
onConfirm: function () {
MuxService.killSession(name)
}
})
}
function createNewSession() {
inputModal.showWithOptions({
title: I18n.tr("New Session"),
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
onConfirm: function (name) {
MuxService.createSession(name)
hide()
}
})
}
function selectNext() {
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
}
function selectPrevious() {
selectedIndex = Math.max(selectedIndex - 1, -1)
}
function activateSelected() {
if (selectedIndex === -1) {
createNewSession()
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
attachToSession(filteredSessions[selectedIndex].name)
}
}
visible: false
modalWidth: 600
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onBackgroundClicked: hide()
Timer {
interval: 3000
running: muxModal.shouldBeVisible
repeat: true
onTriggered: MuxService.refreshSessions()
}
IpcHandler {
function open(): string {
muxModal.show()
return "MUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "MUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "MUX_TOGGLE_SUCCESS"
}
target: "mux"
}
// Backwards compatibility
IpcHandler {
function open(): string {
muxModal.show()
return "TMUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "TMUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "TMUX_TOGGLE_SUCCESS"
}
target: "tmux"
}
InputModal {
id: inputModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
ConfirmModal {
id: confirmModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
directContent: Item {
id: muxPanel
clip: false
property alias searchField: searchField
Keys.onPressed: event => {
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Down)) {
selectNext()
event.accepted = true
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Up)) {
selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
createNewSession()
event.accepted = true
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
renameSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
killSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
activateSelected()
event.accepted = true
}
}
Column {
width: parent.width - Theme.spacingM * 2
height: parent.height - Theme.spacingM * 2
x: Theme.spacingM
y: Theme.spacingM
spacing: Theme.spacingS
// Header
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
// Search field
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search sessions...")
keyForwardTargets: [muxPanel]
onTextEdited: {
muxModal.searchText = text
muxModal.selectedIndex = 0
}
}
// New Session Button
Rectangle {
width: parent.width
height: 56
radius: Theme.cornerRadius
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: Theme.primaryContainer
DankIcon {
anchors.centerIn: parent
name: "add"
size: Theme.iconSize
color: Theme.primary
}
}
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: I18n.tr("New Session")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
MouseArea {
id: newMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.createNewSession()
}
}
// Sessions List
Rectangle {
width: parent.width
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
radius: Theme.cornerRadius
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: muxModal.filteredSessions
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 64
radius: Theme.cornerRadius
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.attachToSession(modelData.name)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
// Avatar
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData.name.charAt(0).toUpperCase()
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: modelData.attached ? Theme.primary : Theme.surfaceText
}
}
// Info
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
text: {
var parts = []
if (modelData.windows !== "N/A")
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
return parts.join(" \u2022 ")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
// Rename button (tmux only)
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
visible: MuxService.supportsRename
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
DankIcon {
anchors.centerIn: parent
name: "edit"
size: Theme.iconSizeSmall
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: renameMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.renameSession(modelData.name)
}
}
// Delete button
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
DankIcon {
anchors.centerIn: parent
name: "delete"
size: Theme.iconSizeSmall
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
muxModal.killSession(modelData.name)
}
}
}
}
}
}
// Empty state
Item {
width: parent.width
height: muxModal.filteredSessions.length === 0 ? 200 : 0
visible: muxModal.filteredSessions.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
size: 48
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
// Shortcuts bar
Row {
id: shortcutsBar
width: parent.width
spacing: Theme.spacingM
bottomPadding: Theme.spacingS
Repeater {
model: {
var shortcuts = [
{ key: "↑↓", label: I18n.tr("Navigate") },
{ key: "↵", label: I18n.tr("Attach") },
{ key: "^N", label: I18n.tr("New") },
{ key: "^D", label: I18n.tr("Kill") },
{ key: "Esc", label: I18n.tr("Close") }
]
if (MuxService.supportsRename)
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
return shortcuts
}
delegate: Row {
required property var modelData
spacing: 4
Rectangle {
width: keyText.width + Theme.spacingS
height: keyText.height + 4
radius: 4
color: Theme.surfaceContainerHighest
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
text: modelData.key
font.pixelSize: Theme.fontSizeSmall - 1
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
@@ -473,5 +473,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: muxLoader
anchors.fill: parent
active: root.currentIndex === 32
visible: active
focus: active
sourceComponent: MuxTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }
@@ -156,7 +156,7 @@ Rectangle {
{ {
"id": "running_apps", "id": "running_apps",
"text": I18n.tr("Running Apps"), "text": I18n.tr("Running Apps"),
"icon": "apps", "icon": "app_registration",
"tabIndex": 19, "tabIndex": 19,
"hyprlandNiriOnly": true "hyprlandNiriOnly": true
}, },
@@ -237,7 +237,7 @@ Rectangle {
{ {
"id": "system", "id": "system",
"text": I18n.tr("System"), "text": I18n.tr("System"),
"icon": "computer", "icon": "memory",
"collapsedByDefault": true, "collapsedByDefault": true,
"children": [ "children": [
{ {
@@ -260,6 +260,12 @@ Rectangle {
"tabIndex": 8, "tabIndex": 8,
"cupsOnly": true "cupsOnly": true
}, },
{
"id": "multiplexers",
"text": I18n.tr("Multiplexers"),
"icon": "terminal",
"tabIndex": 32
},
{ {
"id": "window_rules", "id": "window_rules",
"text": I18n.tr("Window Rules"), "text": I18n.tr("Window Rules"),
@@ -34,7 +34,9 @@ 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.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
DankActionButton { DankActionButton {
anchors.top: parent.top anchors.top: parent.top
@@ -27,12 +27,12 @@ Rectangle {
} }
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
color: isActive ? _tileBgActive : _tileBgInactive color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : 1 border.width: isActive ? 1 : Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
function hoverTint(base) { function hoverTint(base) {
@@ -507,7 +507,8 @@ Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
height: 14 height: 14
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
} }
} }
} }
@@ -529,7 +530,8 @@ Column {
instanceId: widgetData.instanceId || "" instanceId: widgetData.instanceId || ""
screenName: root.screenName screenName: root.screenName
parentScreen: root.parentScreen parentScreen: root.parentScreen
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
onIconClicked: { onIconClicked: {
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) { if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
@@ -552,7 +554,8 @@ Column {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
height: 14 height: 14
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) sliderTrackColor: Theme.ccSliderTrackColor
sliderTrackOpacity: Theme.ccSliderTrackOpacity
} }
} }
} }
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import Quickshell
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -10,7 +11,11 @@ Row {
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
property var availableWidgets: [] property var availableWidgets: []
property Item popoutContent: null property var popupScreen: null
property real popoutX: 0
property real popoutY: 0
property real popoutWidth: 0
property real popoutHeight: 0
signal addWidget(string widgetId) signal addWidget(string widgetId)
signal resetToDefault signal resetToDefault
@@ -19,121 +24,190 @@ Row {
height: 48 height: 48
spacing: Theme.spacingS spacing: Theme.spacingS
onAddWidget: addWidgetPopup.close() function openWidgetLibrary() {
if (popupScreen)
addWidgetWindow.screen = popupScreen;
addWidgetWindow.visible = true;
}
Popup { function closeWidgetLibrary() {
id: addWidgetPopup addWidgetWindow.visible = false;
parent: popoutContent }
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle { onAddWidget: closeWidgetLibrary()
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) onVisibleChanged: {
border.color: Theme.primarySelected if (!visible)
border.width: 0 closeWidgetLibrary();
radius: Theme.cornerRadius }
PanelWindow {
id: addWidgetWindow
screen: root.popupScreen
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:control-center-widget-library"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
} }
contentItem: Item { readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.24 : 0.72) : Theme.popupTransparency
readonly property real rowAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.10 : 0.52) : Theme.popupTransparency
readonly property int panelWidth: 400
readonly property int panelHeight: 300
WindowBlur {
targetWindow: addWidgetWindow
blurX: widgetLibraryPanel.x
blurY: widgetLibraryPanel.y
blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0
blurHeight: addWidgetWindow.visible ? widgetLibraryPanel.height : 0
blurRadius: Theme.cornerRadius
}
MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingL acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.closeWidgetLibrary()
}
Row { FocusScope {
id: headerRow anchors.fill: parent
anchors.top: parent.top focus: addWidgetWindow.visible
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.spacingM
DankIcon { Keys.onEscapePressed: event => {
name: "add_circle" root.closeWidgetLibrary();
size: Theme.iconSize event.accepted = true;
color: Theme.primary }
anchors.verticalCenter: parent.verticalCenter }
}
Typography { Rectangle {
text: I18n.tr("Add Widget") id: widgetLibraryPanel
style: Typography.Style.Subtitle
color: Theme.surfaceText width: addWidgetWindow.panelWidth
anchors.verticalCenter: parent.verticalCenter height: addWidgetWindow.panelHeight
} x: Math.round((root.popoutWidth > 0 ? root.popoutX + (root.popoutWidth - width) / 2 : (addWidgetWindow.width - width) / 2))
y: Math.round((root.popoutHeight > 0 ? root.popoutY + (root.popoutHeight - height) / 2 : (addWidgetWindow.height - height) / 2))
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, addWidgetWindow.surfaceAlpha)
border.color: addWidgetWindow.blurActive ? Theme.outlineMedium : Theme.primarySelected
border.width: addWidgetWindow.blurActive ? Theme.layerOutlineWidth : 0
antialiasing: true
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => mouse.accepted = true
} }
DankListView { Item {
anchors.top: headerRow.bottom anchors.fill: parent
anchors.topMargin: Theme.spacingM anchors.margins: Theme.spacingL
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingS
clip: true
model: root.availableWidgets
delegate: Rectangle { Row {
width: 400 - Theme.spacingL * 2 id: headerRow
height: 50 anchors.top: parent.top
radius: Theme.cornerRadius anchors.left: parent.left
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) anchors.right: parent.right
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) spacing: Theme.spacingM
border.width: 0
Row { DankIcon {
anchors.fill: parent name: "add_circle"
anchors.margins: Theme.spacingM size: Theme.iconSize
spacing: Theme.spacingM color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
Typography {
text: modelData.text
style: Typography.Style.Body
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Typography {
text: modelData.description
style: Typography.Style.Caption
color: Theme.outline
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
} }
MouseArea { Typography {
id: widgetMouseArea text: I18n.tr("Add Widget")
anchors.fill: parent style: Typography.Style.Subtitle
hoverEnabled: true color: Theme.surfaceText
cursorShape: Qt.PointingHandCursor anchors.verticalCenter: parent.verticalCenter
onClicked: { }
root.addWidget(modelData.id); }
DankListView {
id: widgetList
anchors.top: headerRow.bottom
anchors.topMargin: Theme.spacingM
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingS
clip: true
model: root.availableWidgets
delegate: Rectangle {
width: widgetList.width
height: 50
radius: Theme.cornerRadius
color: widgetMouseArea.containsMouse ? Theme.withAlpha(Theme.primary, addWidgetWindow.blurActive ? 0.12 : 0.08) : Theme.withAlpha(Theme.surfaceContainerHigh, addWidgetWindow.rowAlpha)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
antialiasing: true
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 3
Typography {
text: modelData.text
style: Typography.Style.Body
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
Typography {
text: modelData.description
style: Typography.Style.Caption
color: Theme.outline
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: widgetMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(modelData.id);
}
} }
} }
} }
@@ -171,7 +245,7 @@ Row {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: addWidgetPopup.open() onClicked: root.openWidgetLibrary()
} }
} }
@@ -21,9 +21,9 @@ Rectangle {
implicitHeight: 70 implicitHeight: 70
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -41,7 +41,7 @@ DankPopout {
} }
} }
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _containerBg: Theme.nestedSurface
function openWithSection(section) { function openWithSection(section) {
StateUtils.openWithSection(root, section); StateUtils.openWithSection(root, section);
@@ -210,7 +210,11 @@ DankPopout {
EditControls { EditControls {
width: parent.width width: parent.width
visible: editMode visible: editMode
popoutContent: controlContent popupScreen: root.screen
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
availableWidgets: { availableWidgets: {
if (!editMode) if (!editMode)
return []; return [];
@@ -18,9 +18,9 @@ Rectangle {
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
Row { Row {
id: headerRow id: headerRow
@@ -123,6 +123,8 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant thumbOutlineColor: Theme.surfaceVariant
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (AudioService.source && AudioService.source.audio) { if (AudioService.source && AudioService.source.audio) {
@@ -18,9 +18,9 @@ Rectangle {
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
Row { Row {
id: headerRow id: headerRow
@@ -132,6 +132,8 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant thumbOutlineColor: Theme.surfaceVariant
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (AudioService.sink && AudioService.sink.audio) { if (AudioService.sink && AudioService.sink.audio) {
@@ -448,6 +450,7 @@ Rectangle {
Item { Item {
id: appVolumeRow id: appVolumeRow
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
anchors.centerIn: parent anchors.centerIn: parent
height: 40 height: 40
@@ -519,7 +522,8 @@ Rectangle {
unit: "%" unit: "%"
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer thumbOutlineColor: Theme.surfaceContainer
trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.ccSliderTrackColor
trackOpacity: appVolumeRow.sliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (modelData) { if (modelData) {
@@ -12,9 +12,9 @@ Rectangle {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
function isActiveProfile(profile) { function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") { if (typeof PowerProfiles === "undefined") {
@@ -153,9 +153,9 @@ 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.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.floatingSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
opacity: modalVisible ? 1 : 0 opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9 scale: modalVisible ? 1 : 0.9
@@ -20,9 +20,9 @@ Rectangle {
return headerRow.height + bluetoothContent.height + Theme.spacingM; return headerRow.height + bluetoothContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
property var bluetoothCodecModalRef: null property var bluetoothCodecModalRef: null
property var devicesBeingPaired: new Set() property var devicesBeingPaired: new Set()
@@ -115,7 +115,7 @@ Rectangle {
height: 36 height: 36
radius: 18 radius: 18
color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent" color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
border.color: adapterEnabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: adapterEnabled ? Theme.primary : Theme.outlineStrong
border.width: 0 border.width: 0
visible: adapterEnabled visible: adapterEnabled
@@ -434,7 +434,7 @@ Rectangle {
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) color: Theme.outlineStrong
visible: pairedRepeater.count > 0 && availableRepeater.count > 0 visible: pairedRepeater.count > 0 && availableRepeater.count > 0
} }
@@ -609,7 +609,7 @@ Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0 border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: Theme.outlineStrong
} }
MenuItem { MenuItem {
@@ -106,9 +106,9 @@ Rectangle {
return brightnessContent.height + Theme.spacingM; return brightnessContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
DankFlickable { DankFlickable {
id: brightnessContent id: brightnessContent
@@ -16,9 +16,9 @@ Rectangle {
implicitHeight: diskContent.height + Theme.spacingM implicitHeight: diskContent.height + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
Component.onCompleted: { Component.onCompleted: {
DgopService.addRef(["diskmounts"]); DgopService.addRef(["diskmounts"]);
@@ -22,9 +22,9 @@ Rectangle {
return headerRow.height + wifiOffContent.height + Theme.spacingM; return headerRow.height + wifiOffContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
Component.onCompleted: { Component.onCompleted: {
NetworkService.addRef(); NetworkService.addRef();
@@ -11,6 +11,7 @@ Row {
property var defaultSink: AudioService.sink property var defaultSink: AudioService.sink
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
height: 40 height: 40
spacing: 0 spacing: 0
@@ -80,7 +81,8 @@ Row {
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.ccSliderTrackColor
trackOpacity: root.sliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (defaultSink?.audio) { if (defaultSink?.audio) {
@@ -13,6 +13,8 @@ Row {
property string instanceId: "" property string instanceId: ""
property string screenName: "" property string screenName: ""
property var parentScreen: null property var parentScreen: null
property color sliderTrackColor: "transparent"
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
signal iconClicked signal iconClicked
@@ -184,7 +186,8 @@ Row {
} }
} }
thumbOutlineColor: Theme.surfaceContainer thumbOutlineColor: Theme.surfaceContainer
trackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.ccSliderTrackColor
trackOpacity: root.sliderTrackOpacity
Binding on value { Binding on value {
value: root.targetBrightness value: root.targetBrightness
@@ -20,9 +20,9 @@ Rectangle {
width: parent ? parent.width : 200 width: parent ? parent.width : 200
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
Row { Row {
@@ -64,6 +64,8 @@ Rectangle {
minimum: Math.round(root.minimumValue * 100) minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100) maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100) value: Math.round(root.value * 100)
trackColor: Theme.ccSliderTrackColor
trackOpacity: Theme.ccSliderTrackOpacity
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0) onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
} }
} }
@@ -29,23 +29,21 @@ Rectangle {
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor); return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
} }
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _containerBg: Theme.ccPillInactiveBg
color: { color: {
const baseColor = bodyMouse.containsMouse ? Theme.primaryPressed : _containerBg; const baseColor = bodyMouse.containsMouse ? Theme.ccPillInactiveHoverBg : _containerBg;
return baseColor; return baseColor;
} }
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10) border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
antialiasing: true antialiasing: true
readonly property color _labelPrimary: Theme.surfaceText readonly property color _labelPrimary: Theme.surfaceText
readonly property color _labelSecondary: Theme.surfaceVariantText readonly property color _labelSecondary: Theme.surfaceVariantText
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: { readonly property color _tileBgInactive: {
const transparency = Theme.popupTransparency; return Theme.ccTileInactiveBg;
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
} }
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
readonly property color _tileRingInactive: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18) readonly property color _tileRingInactive: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
@@ -92,8 +90,8 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: _tileRadius radius: _tileRadius
color: isActive ? _tileBgActive : _tileBgInactive color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : "transparent" border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : 0 border.width: isActive ? 1 : Theme.layerOutlineWidth
antialiasing: true antialiasing: true
Rectangle { Rectangle {
@@ -11,6 +11,7 @@ Row {
property var defaultSource: AudioService.source property var defaultSource: AudioService.source
property color sliderTrackColor: "transparent" property color sliderTrackColor: "transparent"
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
height: 40 height: 40
spacing: 0 spacing: 0
@@ -73,7 +74,8 @@ Row {
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.ccSliderTrackColor
trackOpacity: root.sliderTrackOpacity
onSliderValueChanged: function (newValue) { onSliderValueChanged: function (newValue) {
if (defaultSource?.audio) { if (defaultSource?.audio) {
SessionData.suppressOSDTemporarily(); SessionData.suppressOSDTemporarily();
@@ -28,7 +28,7 @@ Rectangle {
} }
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
readonly property color _tileIconActive: Theme.ccTileActiveText readonly property color _tileIconActive: Theme.ccTileActiveText
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
@@ -36,11 +36,11 @@ Rectangle {
color: { color: {
if (isActive) if (isActive)
return _tileBgActive; return _tileBgActive;
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive; const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
return baseColor; return baseColor;
} }
border.color: isActive ? _tileRingActive : "transparent" border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : 0 border.width: isActive ? 1 : Theme.layerOutlineWidth
antialiasing: true antialiasing: true
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
@@ -38,11 +38,11 @@ Rectangle {
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor); return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
} }
readonly property color _tileBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _tileBg: Theme.ccPillInactiveBg
color: mouseArea.containsMouse ? Theme.primaryPressed : _tileBg color: mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBg
border.color: "transparent" border.color: Theme.outlineMedium
border.width: 0 border.width: Theme.layerOutlineWidth
antialiasing: true antialiasing: true
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
@@ -26,7 +26,7 @@ Rectangle {
} }
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
readonly property color _tileIconActive: Theme.ccTileActiveText readonly property color _tileIconActive: Theme.ccTileActiveText
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
@@ -34,11 +34,11 @@ Rectangle {
color: { color: {
if (isActive) if (isActive)
return _tileBgActive; return _tileBgActive;
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive; const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
return baseColor; return baseColor;
} }
border.color: isActive ? _tileRingActive : "transparent" border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : 0 border.width: isActive ? 1 : Theme.layerOutlineWidth
antialiasing: true antialiasing: true
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
@@ -26,17 +26,17 @@ Rectangle {
} }
readonly property color _tileBgActive: Theme.ccTileActiveBg readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing readonly property color _tileRingActive: Theme.ccTileRing
color: { color: {
if (isActive) if (isActive)
return _tileBgActive; return _tileBgActive;
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive; const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
return baseColor; return baseColor;
} }
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: 0 border.width: isActive ? 1 : Theme.layerOutlineWidth
opacity: enabled ? 1.0 : 0.6 opacity: enabled ? 1.0 : 0.6
function hoverTint(base) { function hoverTint(base) {
@@ -44,7 +44,7 @@ Rectangle {
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor); return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
} }
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) readonly property color _containerBg: Theme.ccPillInactiveBg
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+1 -1
View File
@@ -54,7 +54,7 @@ Item {
} }
readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0 readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0
readonly property bool shadowEnabled: shadowIntensity > 0 readonly property bool shadowEnabled: !BlurService.enabled && shadowIntensity > 0
readonly property int blurMax: 64 readonly property int blurMax: 64
readonly property real shadowBlurPx: shadowIntensity * 0.2 readonly property real shadowBlurPx: shadowIntensity * 0.2
readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax)) readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax))
@@ -260,7 +260,7 @@ DankPopout {
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.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -295,7 +295,7 @@ DankPopout {
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.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -346,7 +346,7 @@ DankPopout {
width: parent.width width: parent.width
height: batteryColumn.implicitHeight + Theme.spacingM * 2 height: batteryColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -416,7 +416,7 @@ DankPopout {
width: (parent.width - Theme.spacingS * 2) / 3 width: (parent.width - Theme.spacingS * 2) / 3
height: 48 height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -453,7 +453,7 @@ DankPopout {
width: (parent.width - Theme.spacingS * 2) / 3 width: (parent.width - Theme.spacingS * 2) / 3
height: 48 height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -482,7 +482,7 @@ DankPopout {
width: (parent.width - Theme.spacingS * 2) / 3 width: (parent.width - Theme.spacingS * 2) / 3
height: 48 height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.width: 0 border.width: 0
Column { Column {
@@ -110,7 +110,7 @@ PanelWindow {
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)) width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.floatingSurface
radius: Theme.cornerRadius radius: Theme.cornerRadius
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: 1 border.width: 1
@@ -42,19 +42,26 @@ BasePill {
const active = ToplevelManager.activeToplevel; const active = ToplevelManager.activeToplevel;
if (!active) { if (!active) {
// Only clear if our tracked window is no longer alive
if (activeWindow) { if (activeWindow) {
const alive = ToplevelManager.toplevels?.values; if (CompositorService.isNiri) {
if (alive && !Array.from(alive).some(t => t === activeWindow)) if (NiriService.currentOutput === (parentScreen?.name ?? ""))
activeWindow = null; activeWindow = null;
} else {
const alive = ToplevelManager.toplevels?.values;
if (alive && !Array.from(alive).some(t => t === activeWindow))
activeWindow = null;
}
} }
return; return;
} }
if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) { if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) {
activeWindow = active; activeWindow = active;
} else if (activeWindow) {
const alive = ToplevelManager.toplevels?.values;
if (alive && !Array.from(alive).some(t => t === activeWindow))
activeWindow = null;
} }
// else: active window is on a different screen so keep the previous value
} }
Component.onCompleted: { Component.onCompleted: {
@@ -65,7 +72,8 @@ BasePill {
Connections { Connections {
target: ToplevelManager target: ToplevelManager
function onActiveToplevelChanged() { function onActiveToplevelChanged() {
root.updateActiveWindow(); if (!CompositorService.isNiri)
root.updateActiveWindow();
} }
} }
@@ -76,6 +84,16 @@ BasePill {
} }
} }
Connections {
target: CompositorService.isNiri ? NiriService : null
function onWindowsChanged() {
root.updateActiveWindow();
}
function onCurrentOutputChanged() {
root.updateActiveWindow();
}
}
Connections { Connections {
target: DesktopEntries target: DesktopEntries
function onApplicationsChanged() { function onApplicationsChanged() {
@@ -107,21 +125,17 @@ BasePill {
} }
readonly property bool hasWindowsOnCurrentWorkspace: { readonly property bool hasWindowsOnCurrentWorkspace: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
let currentWorkspaceId = null; if (!activeWindow || !(activeWindow.title || activeWindow.appId))
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i];
if (ws.is_focused) {
currentWorkspaceId = ws.id;
break;
}
}
if (!currentWorkspaceId) {
return false; return false;
} if (NiriService.currentOutput !== (parentScreen?.name ?? ""))
return true;
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId); const focusedWin = NiriService.windows.find(w => w.is_focused);
return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId); if (!focusedWin)
return false;
const screenWsIds = new Set(
NiriService.allWorkspaces.filter(ws => ws.output === parentScreen.name).map(ws => ws.id)
);
return screenWsIds.has(focusedWin.workspace_id);
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
+39 -11
View File
@@ -10,6 +10,36 @@ BasePill {
readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null readonly property bool playerAvailable: activePlayer !== null
readonly property bool _hoverPreview: MprisController.isFirefoxYoutubeHoverPreview(activePlayer)
readonly property bool _isPlaying: !!activePlayer && activePlayer.playbackState === 1 && !_hoverPreview
property string _stableTitle: ""
property string _stableArtist: ""
Connections {
target: root.activePlayer
function onTrackTitleChanged() {
root._syncMeta();
}
function onTrackArtistChanged() {
root._syncMeta();
}
}
onActivePlayerChanged: _syncMeta()
function _syncMeta() {
if (!activePlayer) {
_stableTitle = "";
_stableArtist = "";
return;
}
if (MprisController.isFirefoxYoutubeHoverPreview(activePlayer))
return;
_stableTitle = activePlayer.trackTitle || "";
_stableArtist = activePlayer.trackArtist || "";
}
readonly property bool __isChromeBrowser: { readonly property bool __isChromeBrowser: {
if (!activePlayer?.identity) if (!activePlayer?.identity)
return false; return false;
@@ -191,15 +221,15 @@ BasePill {
height: 24 height: 24
radius: 12 radius: 12
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover color: root._isPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3 opacity: activePlayer ? 1 : 0.3
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" name: root._isPlaying ? "pause" : "play_arrow"
size: 14 size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary color: root._isPlaying ? Theme.background : Theme.primary
} }
MouseArea { MouseArea {
@@ -258,12 +288,10 @@ BasePill {
readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari") readonly property bool isWebMedia: lowerIdentity.includes("firefox") || lowerIdentity.includes("chrome") || lowerIdentity.includes("chromium") || lowerIdentity.includes("edge") || lowerIdentity.includes("safari")
property string displayText: { property string displayText: {
if (!activePlayer || !activePlayer.trackTitle) { if (!activePlayer || !root._stableTitle)
return ""; return "";
} const title = isWebMedia ? root._stableTitle : (root._stableTitle || "Unknown Track");
const subtitle = isWebMedia ? (root._stableArtist || cachedIdentity) : (root._stableArtist || "");
const title = isWebMedia ? activePlayer.trackTitle : (activePlayer.trackTitle || "Unknown Track");
const subtitle = isWebMedia ? (activePlayer.trackArtist || cachedIdentity) : (activePlayer.trackArtist || "");
return subtitle.length > 0 ? title + " • " + subtitle : title; return subtitle.length > 0 ? title + " • " + subtitle : title;
} }
@@ -381,15 +409,15 @@ BasePill {
height: 24 height: 24
radius: 12 radius: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover color: root._isPlaying ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3 opacity: activePlayer ? 1 : 0.3
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow" name: root._isPlaying ? "pause" : "play_arrow"
size: 14 size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary color: root._isPlaying ? Theme.background : Theme.primary
} }
MouseArea { MouseArea {
@@ -271,7 +271,7 @@ 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 ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -526,7 +526,7 @@ 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 ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -17,8 +17,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()) : [];
@@ -41,6 +44,54 @@ 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);
}
// ! 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");
@@ -79,6 +130,66 @@ 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 string trayIconTintMode: {
const configuredMode = SettingsData.systemTrayIconTintMode || "none";
switch (configuredMode) {
case "monochrome":
case "primary":
case "secondary":
return configuredMode;
default:
return "none";
}
}
readonly property bool trayIconTintEnabled: trayIconTintMode !== "none"
readonly property real trayIconTintSaturationAmount: {
const raw = SettingsData.systemTrayIconTintSaturation;
const value = (raw === undefined || raw === null) ? 50 : raw;
return Math.max(0, Math.min(100, value)) / 100;
}
readonly property real trayIconTintStrengthAmount: {
const raw = SettingsData.systemTrayIconTintStrength;
const value = (raw === undefined || raw === null) ? 135 : raw;
return Math.max(0, Math.min(200, value)) / 100;
}
readonly property real trayIconSaturation: {
switch (trayIconTintMode) {
case "monochrome":
return -1;
case "primary":
case "secondary":
return -root.trayIconTintSaturationAmount;
default:
return 0;
}
}
readonly property real trayIconColorization: {
switch (trayIconTintMode) {
case "primary":
case "secondary":
return root.trayIconTintStrengthAmount;
default:
return 0;
}
}
readonly property color trayIconTintColor: {
switch (trayIconTintMode) {
case "primary":
return Theme.primary;
case "secondary":
return Theme.secondary;
default:
return Theme.surfaceText;
}
}
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)
@@ -104,6 +215,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
@@ -290,6 +402,12 @@ BasePill {
smooth: true smooth: true
mipmap: true mipmap: true
visible: status === Image.Ready visible: status === Image.Ready
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
} }
Text { Text {
@@ -435,6 +553,313 @@ 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
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
}
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
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
}
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);
}
}
} }
} }
@@ -550,6 +975,10 @@ BasePill {
smooth: true smooth: true
mipmap: true mipmap: true
visible: status === Image.Ready visible: status === Image.Ready
layer.enabled: root.trayIconsMonochrome && visible
layer.effect: MultiEffect {
saturation: -1
}
} }
Text { Text {
@@ -976,7 +1405,7 @@ BasePill {
Item { Item {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
layer.enabled: true layer.enabled: !BlurService.enabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1067,6 +1496,12 @@ BasePill {
smooth: true smooth: true
mipmap: true mipmap: true
visible: status === Image.Ready visible: status === Image.Ready
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
} }
Text { Text {
@@ -1466,7 +1901,7 @@ BasePill {
Item { Item {
id: menuBgShadowLayer id: menuBgShadowLayer
anchors.fill: parent anchors.fill: parent
layer.enabled: true layer.enabled: !BlurService.enabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -82,6 +82,7 @@ DankPopout {
isRightEdge: root.__dropdownRightEdge isRightEdge: root.__dropdownRightEdge
activePlayer: root.__dropdownPlayer activePlayer: root.__dropdownPlayer
allPlayers: root.__dropdownPlayers allPlayers: root.__dropdownPlayers
targetWindow: root.backgroundWindow
onCloseRequested: root.__hideDropdowns() onCloseRequested: root.__hideDropdowns()
onPanelEntered: root.__stopCloseTimer() onPanelEntered: root.__stopCloseTimer()
onPanelExited: root.__startCloseTimer() onPanelExited: root.__startCloseTimer()
@@ -16,6 +16,7 @@ Item {
property var allPlayers: [] property var allPlayers: []
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false property bool isRightEdge: false
property var targetWindow: null
property bool __isChromeBrowser: { property bool __isChromeBrowser: {
if (!activePlayer?.identity) if (!activePlayer?.identity)
@@ -57,6 +58,30 @@ Item {
}); });
} }
readonly property Item __activePanel: {
switch (dropdownType) {
case 1:
return volumePanel;
case 2:
return audioDevicesPanel;
case 3:
return playersPanel;
default:
return null;
}
}
WindowBlur {
targetWindow: root.targetWindow
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1
blurX: root.__activePanel ? root.__activePanel.x + root.__activePanel.width * (1 - s) * 0.5 : 0
blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
blurWidth: active ? root.__activePanel.width * s : 0
blurHeight: active ? root.__activePanel.height * s : 0
blurRadius: Theme.cornerRadius * 2
}
Rectangle { Rectangle {
id: volumePanel id: volumePanel
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
@@ -65,8 +90,8 @@ Item {
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Theme.floatingSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Theme.outlineStrong
border.width: 1 border.width: 1
opacity: dropdownType === 1 ? 1 : 0 opacity: dropdownType === 1 ? 1 : 0
@@ -89,7 +114,7 @@ Item {
} }
} }
layer.enabled: true layer.enabled: !BlurService.enabled
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
@@ -123,23 +148,26 @@ Item {
width: parent.width width: parent.width
height: parent.height height: parent.height
anchors.centerIn: parent anchors.centerIn: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.outline, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }
Rectangle { Rectangle {
readonly property real ratio: volumeAvailable ? Math.min(1.0, currentVolume) : 0
readonly property real thumbHeight: 4
width: parent.width width: parent.width
height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0 height: Math.max(0, ratio * (parent.height - thumbHeight) - 3)
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary color: Theme.primary
bottomLeftRadius: Theme.cornerRadius radius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius topLeftRadius: 0
topRightRadius: 0
} }
Rectangle { Rectangle {
width: parent.width + 8 width: parent.width + 8
height: 8 height: 4
radius: Theme.cornerRadius radius: Theme.cornerRadius
y: { y: {
const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0; const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
@@ -148,8 +176,7 @@ Item {
} }
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary color: Theme.primary
border.width: 3 border.width: 0
border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
} }
MouseArea { MouseArea {
@@ -199,8 +226,8 @@ Item {
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Theme.floatingSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Theme.outlineStrong
border.width: 2 border.width: 2
opacity: dropdownType === 2 ? 1 : 0 opacity: dropdownType === 2 ? 1 : 0
@@ -223,7 +250,7 @@ Item {
} }
} }
layer.enabled: true layer.enabled: !BlurService.enabled
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
@@ -267,7 +294,7 @@ Item {
width: parent.width width: parent.width
height: 48 height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.nestedSurface
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === AudioService.sink ? 2 : 1 border.width: modelData === AudioService.sink ? 2 : 1
@@ -349,8 +376,8 @@ Item {
x: isRightEdge ? anchorPos.x : anchorPos.x - width x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Theme.floatingSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Theme.outlineStrong
border.width: 2 border.width: 2
opacity: dropdownType === 3 ? 1 : 0 opacity: dropdownType === 3 ? 1 : 0
@@ -373,7 +400,7 @@ Item {
} }
} }
layer.enabled: true layer.enabled: !BlurService.enabled
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
@@ -417,7 +444,7 @@ Item {
width: parent.width width: parent.width
height: 48 height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.nestedSurface
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1 border.width: modelData === activePlayer ? 2 : 1
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -638,7 +637,7 @@ Item {
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185 y: 185
color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
visible: (allPlayers?.length || 0) >= 1 visible: (allPlayers?.length || 0) >= 1
@@ -681,7 +680,7 @@ Item {
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130 y: 130
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15) border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
border.width: 1 border.width: 1
z: 101 z: 101
enabled: volumeAvailable enabled: volumeAvailable
@@ -758,7 +757,7 @@ Item {
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240 y: 240
color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
@@ -83,8 +83,8 @@ Rectangle {
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) border.color: Theme.outlineMedium
border.width: 1 border.width: 1
Column { Column {
@@ -351,7 +351,7 @@ Rectangle {
} else if (eventMouseArea.containsMouse) { } else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06); return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
} }
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return Theme.nestedSurface;
} }
border.color: { border.color: {
if (modelData.url && eventMouseArea.containsMouse) { if (modelData.url && eventMouseArea.containsMouse) {
@@ -359,9 +359,9 @@ Rectangle {
} else if (eventMouseArea.containsMouse) { } else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15); return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
} }
return "transparent"; return Theme.outlineMedium;
} }
border.width: 1 border.width: eventMouseArea.containsMouse ? 1 : Theme.layerOutlineWidth
Rectangle { Rectangle {
width: 3 width: 3
@@ -10,8 +10,8 @@ Rectangle {
property int pad: Theme.spacingM property int pad: Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Theme.outlineMedium
border.width: 1 border.width: 1
default property alias content: contentItem.data default property alias content: contentItem.data
@@ -103,7 +103,7 @@ Rectangle {
} }
] ]
color: isCurrent ? Theme.withAlpha(Theme.primary, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: isCurrent ? Theme.withAlpha(Theme.primary, 0.1) : Theme.nestedSurface
border.color: isCurrent ? Theme.withAlpha(Theme.primary, 0.3) : "transparent" border.color: isCurrent ? Theme.withAlpha(Theme.primary, 0.3) : "transparent"
border.width: isCurrent ? 1 : 0 border.width: isCurrent ? 1 : 0
+2 -2
View File
@@ -213,8 +213,8 @@ Item {
width: parent.width width: parent.width
height: heroContent.height + Theme.spacingL * 2 height: heroContent.height + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Theme.withAlpha(Theme.outline, 0.08) border.color: Theme.outlineMedium
border.width: 1 border.width: 1
Column { Column {
+8 -24
View File
@@ -29,12 +29,6 @@ Singleton {
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool useFahrenheit: false property bool useFahrenheit: false
property bool nightModeEnabled: false property bool nightModeEnabled: false
property string weatherLocation: "New York, NY" property string weatherLocation: "New York, NY"
@@ -87,12 +81,6 @@ Singleton {
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true; use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false; showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false; padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false; useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false; nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"; weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
@@ -149,27 +137,23 @@ Singleton {
} }
function getEffectiveTimeFormat() { function getEffectiveTimeFormat() {
const use24 = greeterUse24HourClock; if (use24HourClock)
const secs = greeterShowSeconds; return showSeconds ? "hh:mm:ss" : "hh:mm";
const pad = greeterPadHours12Hour; if (padHours12Hour)
if (use24) return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "hh:mm:ss" : "hh:mm"; return showSeconds ? "h:mm:ss AP" : "h:mm AP";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
} }
function getEffectiveLockDateFormat() { function getEffectiveLockDateFormat() {
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat; return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
} }
function getEffectiveWallpaperFillMode() { function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode; return wallpaperFillMode;
} }
function getEffectiveFontFamily() { function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily; return fontFamily;
} }
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
+8
View File
@@ -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
+34 -45
View File
@@ -23,6 +23,7 @@ Item {
property string passwordBuffer: "" property string passwordBuffer: ""
property bool demoMode: false property bool demoMode: false
property var pam: demoPam
property string screenName: "" property string screenName: ""
property bool unlocking: false property bool unlocking: false
property string pamState: "" property string pamState: ""
@@ -52,20 +53,18 @@ Item {
return I18n.tr("Touch your security key..."); return I18n.tr("Touch your security key...");
if (pam.lockMessage && pam.lockMessage.length > 0) if (pam.lockMessage && pam.lockMessage.length > 0)
return pam.lockMessage; return pam.lockMessage;
if (pam.fprintState === "error") {
const detail = (pam.fprint.message || "").trim();
return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
}
if (pam.fprintState === "max")
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
if (pam.fprintState === "fail")
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
if (root.pamState === "error") if (root.pamState === "error")
return I18n.tr("Authentication error - try again"); return I18n.tr("Authentication error - try again");
if (root.pamState === "max") if (root.pamState === "max")
return I18n.tr("Too many attempts - locked out"); return I18n.tr("Too many attempts - locked out");
if (root.pamState === "fail") if (root.pamState === "fail")
return I18n.tr("Incorrect password - try again"); return I18n.tr("Incorrect password - try again");
if (pam.fprintState === "error")
return I18n.tr("Fingerprint error");
if (pam.fprintState === "max")
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
if (pam.fprintState === "fail")
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
return ""; return "";
} }
@@ -745,13 +744,6 @@ Item {
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
} }
@@ -1639,49 +1631,46 @@ Item {
} }
Pam { Pam {
id: pam id: demoPam
lockSecured: !demoMode lockSecured: false
onUnlockRequested: { }
Connections {
target: root.pam
function onUnlockRequested() {
root.unlocking = true; root.unlocking = true;
lockerReadyArmed = false; lockerReadyArmed = false;
passwordField.text = ""; passwordField.text = "";
root.passwordBuffer = ""; root.passwordBuffer = "";
root.unlockRequested(); root.unlockRequested();
} }
onStateChanged: {
root.pamState = state;
if (state !== "") {
root.unlocking = false;
placeholderDelay.restart();
passwordField.text = "";
root.passwordBuffer = "";
}
}
onU2fPendingChanged: {
if (u2fPending) {
passwordField.text = "";
root.passwordBuffer = "";
if (keyboardController.isKeyboardActive)
keyboardController.hide();
}
}
}
Connections { function onStateChanged() {
target: pam root.pamState = root.pam.state;
if (root.pam.state === "")
return;
root.unlocking = false;
placeholderDelay.restart();
passwordField.text = "";
root.passwordBuffer = "";
}
function onU2fPendingChanged() {
if (!root.pam.u2fPending)
return;
passwordField.text = "";
root.passwordBuffer = "";
if (keyboardController.isKeyboardActive)
keyboardController.hide();
}
function onUnlockInProgressChanged() { function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking) if (!root.pam.unlockInProgress && root.unlocking)
root.unlocking = false; root.unlocking = false;
} }
} }
Binding {
target: pam
property: "buffer"
value: root.passwordBuffer
}
Timer { Timer {
id: placeholderDelay id: placeholderDelay
+2
View File
@@ -7,6 +7,7 @@ Rectangle {
id: root id: root
required property WlSessionLock lock required property WlSessionLock lock
required property var pam
required property string sharedPasswordBuffer required property string sharedPasswordBuffer
required property string screenName required property string screenName
required property bool isLocked required property bool isLocked
@@ -21,6 +22,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
demoMode: false demoMode: false
pam: root.pam
passwordBuffer: root.sharedPasswordBuffer passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName screenName: root.screenName
onUnlockRequested: root.unlockRequested() onUnlockRequested: root.unlockRequested()
+25 -23
View File
@@ -179,6 +179,8 @@ Scope {
abort(); abort();
return; return;
} }
if (active)
return;
tries = 0; tries = 0;
errorTries = 0; errorTries = 0;
@@ -192,22 +194,23 @@ Scope {
if (!available) if (!available)
return; return;
if (res === PamResult.Success) { switch (res) {
case PamResult.Success:
if (!root.unlockInProgress) { if (!root.unlockInProgress) {
passwd.abort(); passwd.abort();
root.proceedAfterPrimaryAuth(); root.proceedAfterPrimaryAuth();
} }
return; return;
} case PamResult.Error:
if (res === PamResult.Error) {
root.fprintState = "error";
errorTries++; errorTries++;
if (errorTries < 5) { if (errorTries < 200) {
abort(); abort();
errorRetry.restart(); errorRetry.restart();
return;
} }
} else if (res === PamResult.MaxTries) { abort();
return;
case PamResult.MaxTries:
tries++; tries++;
if (tries < SettingsData.maxFprintTries) { if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail"; root.fprintState = "fail";
@@ -216,6 +219,9 @@ Scope {
root.fprintState = "max"; root.fprintState = "max";
abort(); abort();
} }
break;
default:
return;
} }
root.flashMsg(); root.flashMsg();
@@ -294,7 +300,7 @@ Scope {
Timer { Timer {
id: errorRetry id: errorRetry
interval: 800 interval: 1500
onTriggered: fprint.start() onTriggered: fprint.start()
} }
@@ -346,26 +352,22 @@ Scope {
id: fprintStateReset id: fprintStateReset
interval: 4000 interval: 4000
onTriggered: { onTriggered: root.fprintState = ""
root.fprintState = "";
fprint.errorTries = 0;
}
} }
onLockSecuredChanged: { onLockSecuredChanged: {
if (lockSecured) { if (!lockSecured) {
SettingsData.refreshAuthAvailability();
root.state = "";
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.lockMessage = "";
root.resetAuthFlows();
fprint.checkAvail();
u2f.checkAvail();
} else {
root.resetAuthFlows(); root.resetAuthFlows();
return;
} }
root.state = "";
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.lockMessage = "";
root.resetAuthFlows();
fprint.checkAvail();
u2f.checkAvail();
} }
Connections { Connections {
@@ -35,14 +35,14 @@ Rectangle {
color: { color: {
if (isSelected && keyboardNavigationActive) if (isSelected && keyboardNavigationActive)
return Theme.primaryPressed; return Theme.primaryPressed;
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return Theme.floatingSurfaceHigh;
} }
border.color: { border.color: {
if (isSelected && keyboardNavigationActive) if (isSelected && keyboardNavigationActive)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5); return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5);
if (historyItem.urgency === 2) if (historyItem.urgency === 2)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3); return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05); return Theme.outlineMedium;
} }
border.width: { border.width: {
if (isSelected && keyboardNavigationActive) if (isSelected && keyboardNavigationActive)
@@ -122,12 +122,10 @@ Rectangle {
return ""; return "";
const appIcon = historyItem.appIcon; const appIcon = historyItem.appIcon;
if (!appIcon) if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; return "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon; return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) return "";
return "";
return Paths.resolveIconPath(appIcon);
} }
hasImage: hasNotificationImage hasImage: hasNotificationImage
@@ -71,7 +71,7 @@ Rectangle {
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight; return Theme.primaryHoverLight;
} }
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return Theme.floatingSurfaceHigh;
} }
border.color: { border.color: {
if (isGroupSelected && keyboardNavigationActive) { if (isGroupSelected && keyboardNavigationActive) {
@@ -83,7 +83,7 @@ Rectangle {
if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3); return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
} }
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05); return Theme.outlineMedium;
} }
border.width: { border.width: {
if (isGroupSelected && keyboardNavigationActive) { if (isGroupSelected && keyboardNavigationActive) {
@@ -169,12 +169,10 @@ Rectangle {
return ""; return "";
const appIcon = notificationGroup?.latestNotification?.appIcon; const appIcon = notificationGroup?.latestNotification?.appIcon;
if (!appIcon) if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; return "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon; return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) return "";
return "";
return Paths.resolveIconPath(appIcon);
} }
hasImage: hasNotificationImage hasImage: hasNotificationImage
@@ -454,8 +452,8 @@ Rectangle {
return expandedBaseHeight; return expandedBaseHeight;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: isSelected ? Theme.primaryPressed : Theme.nestedSurface
border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.outlineMedium
border.width: 1 border.width: 1
Behavior on border.color { Behavior on border.color {
@@ -503,12 +501,10 @@ Rectangle {
return ""; return "";
const appIcon = modelData?.appIcon; const appIcon = modelData?.appIcon;
if (!appIcon) if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; return "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon; return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) return "";
return "";
return Paths.resolveIconPath(appIcon);
} }
fallbackIcon: { fallbackIcon: {
@@ -678,8 +674,10 @@ Rectangle {
onEntered: parent.isHovered = true onEntered: parent.isHovered = true
onExited: parent.isHovered = false onExited: parent.isHovered = false
onClicked: { onClicked: {
if (modelData && modelData.invoke) if (modelData && modelData.invoke) {
modelData.invoke(); modelData.invoke();
PopoutService.closeNotificationCenter();
}
} }
} }
} }
@@ -817,6 +815,7 @@ Rectangle {
onClicked: { onClicked: {
if (modelData && modelData.invoke) { if (modelData && modelData.invoke) {
modelData.invoke(); modelData.invoke();
PopoutService.closeNotificationCenter();
} }
} }
} }
@@ -97,6 +97,8 @@ DankPopout {
onDprChanged: updateStablePopupHeight() onDprChanged: updateStablePopupHeight()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
notificationHistoryVisible = shouldBeVisible;
if (shouldBeVisible) { if (shouldBeVisible) {
NotificationService.onOverlayOpen(); NotificationService.onOverlayOpen();
updateStablePopupHeight(); updateStablePopupHeight();
@@ -99,7 +99,9 @@ Item {
height: Theme.iconSize + Theme.spacingS height: Theme.iconSize + Theme.spacingS
radius: Theme.cornerRadius radius: Theme.cornerRadius
visible: root.currentTab === 0 ? NotificationService.notifications.length > 0 : NotificationService.historyList.length > 0 visible: root.currentTab === 0 ? NotificationService.notifications.length > 0 : NotificationService.historyList.length > 0
color: clearArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: clearArea.containsMouse ? Theme.primaryHoverLight : Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Row { Row {
id: clearButtonContent id: clearButtonContent
@@ -14,8 +14,8 @@ Rectangle {
visible: expanded visible: expanded
clip: true clip: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) color: Theme.nestedSurface
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) border.color: Theme.outlineMedium
border.width: 1 border.width: 1
Behavior on height { Behavior on height {
@@ -325,8 +325,9 @@ PanelWindow {
property bool swipeDismissing: false property bool swipeDismissing: false
readonly property real radiusForShadow: Theme.cornerRadius readonly property real radiusForShadow: Theme.cornerRadius
property real shadowBlurPx: SettingsData.notificationPopupShadowEnabled ? ((2 + radiusForShadow * 0.2) * (cardHoverHandler.hovered ? 1.2 : 1)) : 0 readonly property bool shadowsAllowed: SettingsData.notificationPopupShadowEnabled && !BlurService.enabled
property real shadowSpreadPx: SettingsData.notificationPopupShadowEnabled ? (radiusForShadow * (cardHoverHandler.hovered ? 0.06 : 0)) : 0 property real shadowBlurPx: shadowsAllowed ? ((2 + radiusForShadow * 0.2) * (cardHoverHandler.hovered ? 1.2 : 1)) : 0
property real shadowSpreadPx: shadowsAllowed ? (radiusForShadow * (cardHoverHandler.hovered ? 0.06 : 0)) : 0
property real shadowBaseAlpha: 0.35 property real shadowBaseAlpha: 0.35
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
@@ -349,7 +350,7 @@ PanelWindow {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.snap(4, win.dpr) anchors.margins: Theme.snap(4, win.dpr)
layer.enabled: !win._isDestroying && win.screenValid layer.enabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
layer.smooth: false layer.smooth: false
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr)) layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -411,9 +412,10 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
anchors.margins: content.cardInset anchors.margins: content.cardInset
radius: Theme.cornerRadius radius: Theme.cornerRadius
antialiasing: true
color: "transparent" color: "transparent"
border.color: BlurService.borderColor border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth border.width: BlurService.enabled ? BlurService.borderWidth : 1
z: 100 z: 100
} }
@@ -500,12 +502,10 @@ PanelWindow {
return ""; return "";
const appIcon = notificationData.appIcon; const appIcon = notificationData.appIcon;
if (!appIcon) if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : ""; return "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/")) if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon; return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:")) return "";
return "";
return Paths.resolveIconPath(appIcon);
} }
hasImage: hasNotificationImage hasImage: hasNotificationImage
+11 -2
View File
@@ -47,6 +47,9 @@ DankOSD {
} }
property bool _pendingShow: false property bool _pendingShow: false
property string _displayTitle: ""
property string _displayArtist: ""
property string _displayAlbum: ""
Timer { Timer {
id: iconDebounce id: iconDebounce
@@ -105,6 +108,12 @@ DankOSD {
return; return;
if (!SettingsData.osdMediaPlaybackEnabled) if (!SettingsData.osdMediaPlaybackEnabled)
return; return;
if (MprisController.isFirefoxYoutubeHoverPreview(player))
return;
root._displayTitle = player.trackTitle || "";
root._displayArtist = player.trackArtist || "";
root._displayAlbum = player.trackAlbum || "";
root.updatePlaybackIcon(); root.updatePlaybackIcon();
TrackArtService.loadArtwork(player.trackArtUrl); TrackArtService.loadArtwork(player.trackArtUrl);
@@ -254,7 +263,7 @@ DankOSD {
StyledText { StyledText {
id: topText id: topText
width: parent.width width: parent.width
text: player ? `${player.trackTitle || I18n.tr("Unknown Title")}` : "" text: player ? (root._displayTitle || I18n.tr("Unknown Title")) : ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -265,7 +274,7 @@ DankOSD {
StyledText { StyledText {
id: bottomText id: bottomText
width: parent.width width: parent.width
text: player ? ((player.trackArtist || I18n.tr("Unknown Artist")) + (player.trackAlbum ? ` ${player.trackAlbum}` : "")) : "" text: player ? ((root._displayArtist || I18n.tr("Unknown Artist")) + (root._displayAlbum ? ` ${root._displayAlbum}` : "")) : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Light font.weight: Font.Light
color: Theme.surfaceText color: Theme.surfaceText
@@ -211,6 +211,7 @@ Item {
property real minWidth: contentLoader.item?.minWidth ?? 100 property real minWidth: contentLoader.item?.minWidth ?? 100
property real minHeight: contentLoader.item?.minHeight ?? 100 property real minHeight: contentLoader.item?.minHeight ?? 100
property bool forceSquare: contentLoader.item?.forceSquare ?? false property bool forceSquare: contentLoader.item?.forceSquare ?? false
property bool acceptsKeyboardFocus: contentLoader.item?.acceptsKeyboardFocus ?? false
property bool isInteracting: dragArea.pressed || resizeArea.pressed property bool isInteracting: dragArea.pressed || resizeArea.pressed
property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings property var _gridSettingsTrigger: SettingsData.desktopWidgetGridSettings
@@ -299,11 +300,14 @@ Item {
} }
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: { WlrLayershell.keyboardFocus: {
if (!root.isInteracting) if (root.isInteracting) {
return WlrKeyboardFocus.None; if (CompositorService.useHyprlandFocusGrab)
if (CompositorService.useHyprlandFocusGrab) return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
if (root.acceptsKeyboardFocus)
return WlrKeyboardFocus.OnDemand; return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive; return WlrKeyboardFocus.None;
} }
HyprlandFocusGrab { HyprlandFocusGrab {
+2 -2
View File
@@ -33,7 +33,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 80 Layout.preferredHeight: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@@ -110,7 +110,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -163,7 +163,7 @@ Item {
property color extraInfoColor: Theme.surfaceVariantText property color extraInfoColor: Theme.surfaceVariantText
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
border.color: Theme.outlineLight border.color: Theme.outlineLight
border.width: 1 border.width: 1
@@ -185,7 +185,7 @@ Popup {
} }
contentItem: Rectangle { contentItem: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.floatingSurface
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : 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: BlurService.enabled ? BlurService.borderWidth : 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
@@ -357,7 +357,7 @@ DankPopout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
clip: true clip: true
ProcessesView { ProcessesView {
@@ -23,7 +23,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2 Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
ColumnLayout { ColumnLayout {
id: systemInfoColumn id: systemInfoColumn
@@ -96,7 +96,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.nestedSurface
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
+447 -340
View File
@@ -52,9 +52,11 @@ Item {
} }
function _isBarActive(c) { function _isBarActive(c) {
if (!c.enabled) return false; if (!c.enabled)
return false;
const prefs = c.screenPreferences || ["all"]; const prefs = c.screenPreferences || ["all"];
if (prefs.length > 0) return true; if (prefs.length > 0)
return true;
return (c.showOnLastDisplay ?? true) && Quickshell.screens.length === 1; return (c.showOnLastDisplay ?? true) && Quickshell.screens.length === 1;
} }
@@ -64,7 +66,8 @@ Item {
return; return;
const hasHorizontal = configs.some(c => { const hasHorizontal = configs.some(c => {
if (!_isBarActive(c)) return false; if (!_isBarActive(c))
return false;
const p = c.position ?? SettingsData.Position.Top; const p = c.position ?? SettingsData.Position.Top;
return p === SettingsData.Position.Top || p === SettingsData.Position.Bottom; return p === SettingsData.Position.Top || p === SettingsData.Position.Bottom;
}); });
@@ -72,7 +75,8 @@ Item {
return; return;
const hasVertical = configs.some(c => { const hasVertical = configs.some(c => {
if (!_isBarActive(c)) return false; if (!_isBarActive(c))
return false;
const p = c.position ?? SettingsData.Position.Top; const p = c.position ?? SettingsData.Position.Top;
return p === SettingsData.Position.Left || p === SettingsData.Position.Right; return p === SettingsData.Position.Left || p === SettingsData.Position.Right;
}); });
@@ -305,7 +309,7 @@ Item {
const prefs = cfg?.screenPreferences || ["all"]; const prefs = cfg?.screenPreferences || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"))
return I18n.tr("All displays"); return I18n.tr("All displays");
return I18n.tr("%1 display(s)").replace("%1", prefs.length); return prefs.length === 1 ? I18n.tr("%1 display").arg(prefs.length) : I18n.tr("%1 displays").arg(prefs.length);
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -413,11 +417,71 @@ Item {
} }
} }
SettingsCard {
iconName: "vertical_align_center"
title: I18n.tr("Position")
settingKey: "barPosition"
visible: selectedBarConfig?.enabled ?? false
Item {
width: parent.width
height: positionButtonGroup.height
DankButtonGroup {
id: positionButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
model: [I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Left"), I18n.tr("Right")]
currentIndex: {
selectedBarId;
const config = SettingsData.getBarConfig(selectedBarId);
const pos = config?.position ?? 0;
switch (pos) {
case SettingsData.Position.Top:
return 0;
case SettingsData.Position.Bottom:
return 1;
case SettingsData.Position.Left:
return 2;
case SettingsData.Position.Right:
return 3;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let newPos = 0;
switch (index) {
case 0:
newPos = SettingsData.Position.Top;
break;
case 1:
newPos = SettingsData.Position.Bottom;
break;
case 2:
newPos = SettingsData.Position.Left;
break;
case 3:
newPos = SettingsData.Position.Right;
break;
}
SettingsData.updateBarConfig(selectedBarId, {
position: newPos
});
notifyHorizontalBarChange();
}
}
}
}
SettingsCard { SettingsCard {
iconName: "display_settings" iconName: "display_settings"
title: I18n.tr("Display Assignment") title: I18n.tr("Display Assignment")
settingKey: "barDisplay" settingKey: "barDisplay"
visible: selectedBarConfig?.enabled collapsible: true
expanded: false
visible: selectedBarConfig?.enabled ?? false
StyledText { StyledText {
width: parent.width width: parent.width
@@ -518,69 +582,13 @@ Item {
} }
} }
SettingsCard {
iconName: "vertical_align_center"
title: I18n.tr("Position")
settingKey: "barPosition"
visible: selectedBarConfig?.enabled
Item {
width: parent.width
height: positionButtonGroup.height
DankButtonGroup {
id: positionButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
model: [I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Left"), I18n.tr("Right")]
currentIndex: {
selectedBarId;
const config = SettingsData.getBarConfig(selectedBarId);
const pos = config?.position ?? 0;
switch (pos) {
case SettingsData.Position.Top:
return 0;
case SettingsData.Position.Bottom:
return 1;
case SettingsData.Position.Left:
return 2;
case SettingsData.Position.Right:
return 3;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let newPos = 0;
switch (index) {
case 0:
newPos = SettingsData.Position.Top;
break;
case 1:
newPos = SettingsData.Position.Bottom;
break;
case 2:
newPos = SettingsData.Position.Left;
break;
case 3:
newPos = SettingsData.Position.Right;
break;
}
SettingsData.updateBarConfig(selectedBarId, {
position: newPos
});
notifyHorizontalBarChange();
}
}
}
}
SettingsCard { SettingsCard {
iconName: "visibility_off" iconName: "visibility_off"
title: I18n.tr("Visibility") title: I18n.tr("Visibility")
settingKey: "barVisibility" settingKey: "barVisibility"
visible: selectedBarConfig?.enabled collapsible: true
expanded: false
visible: selectedBarConfig?.enabled ?? false
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Auto-hide") text: I18n.tr("Auto-hide")
@@ -695,106 +703,11 @@ Item {
} }
} }
SettingsToggleCard {
iconName: "fit_screen"
title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized")
visible: selectedBarConfig?.enabled && (CompositorService.isNiri || CompositorService.isHyprland)
checked: selectedBarConfig?.maximizeDetection ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeDetection: checked
})
}
SettingsToggleCard {
iconName: "mouse"
title: I18n.tr("Scroll Wheel")
description: I18n.tr("Control workspaces and columns by scrolling on the bar")
visible: selectedBarConfig?.enabled
checked: selectedBarConfig?.scrollEnabled ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
scrollEnabled: checked
})
SettingsButtonGroupRow {
text: I18n.tr("Y Axis")
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
currentIndex: {
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
case "none":
return 0;
case "workspace":
return 1;
case "column":
return 2;
default:
return 1;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let behavior = "workspace";
switch (index) {
case 0:
behavior = "none";
break;
case 1:
behavior = "workspace";
break;
case 2:
behavior = "column";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
scrollYBehavior: behavior
});
}
}
SettingsButtonGroupRow {
text: I18n.tr("X Axis")
visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
currentIndex: {
switch (selectedBarConfig?.scrollXBehavior || "column") {
case "none":
return 0;
case "workspace":
return 1;
case "column":
return 2;
default:
return 2;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let behavior = "column";
switch (index) {
case 0:
behavior = "none";
break;
case 1:
behavior = "workspace";
break;
case 2:
behavior = "column";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
scrollXBehavior: behavior
});
}
}
}
SettingsCard { SettingsCard {
iconName: "space_bar" iconName: "space_bar"
title: I18n.tr("Spacing") title: I18n.tr("Spacing")
settingKey: "barSpacing" settingKey: "barSpacing"
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled ?? false
SettingsSliderRow { SettingsSliderRow {
id: edgeSpacingSlider id: edgeSpacingSlider
@@ -937,61 +850,11 @@ Item {
} }
} }
SettingsSliderCard {
id: fontScaleSliderCard
iconName: "text_fields"
title: I18n.tr("Font Scale")
description: I18n.tr("Scale DankBar font sizes independently")
visible: selectedBarConfig?.enabled
minimum: 50
maximum: 200
value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100)
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => {
SettingsData.updateBarConfig(selectedBarId, {
fontScale: newValue / 100
});
}
Binding {
target: fontScaleSliderCard
property: "value"
value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100)
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderCard {
id: iconScaleSliderCard
iconName: "interests"
title: I18n.tr("Icon Scale")
description: I18n.tr("Scale DankBar icon sizes independently")
visible: selectedBarConfig?.enabled
minimum: 50
maximum: 200
value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100)
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => {
SettingsData.updateBarConfig(selectedBarId, {
iconScale: newValue / 100
});
}
Binding {
target: iconScaleSliderCard
property: "value"
value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100)
restoreMode: Binding.RestoreBinding
}
}
SettingsCard { SettingsCard {
iconName: "opacity" iconName: "opacity"
title: I18n.tr("Transparency") title: I18n.tr("Transparency")
settingKey: "barTransparency" settingKey: "barTransparency"
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled ?? false
SettingsSliderRow { SettingsSliderRow {
id: barTransparencySlider id: barTransparencySlider
@@ -1038,13 +901,63 @@ Item {
} }
} }
SettingsSliderCard {
id: fontScaleSliderCard
iconName: "text_fields"
title: I18n.tr("Font Scale")
description: I18n.tr("Scale DankBar font sizes independently")
visible: selectedBarConfig?.enabled ?? false
minimum: 50
maximum: 200
value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100)
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => {
SettingsData.updateBarConfig(selectedBarId, {
fontScale: newValue / 100
});
}
Binding {
target: fontScaleSliderCard
property: "value"
value: Math.round((selectedBarConfig?.fontScale ?? 1.0) * 100)
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderCard {
id: iconScaleSliderCard
iconName: "interests"
title: I18n.tr("Icon Scale")
description: I18n.tr("Scale DankBar icon sizes independently")
visible: selectedBarConfig?.enabled ?? false
minimum: 50
maximum: 200
value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100)
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => {
SettingsData.updateBarConfig(selectedBarId, {
iconScale: newValue / 100
});
}
Binding {
target: iconScaleSliderCard
property: "value"
value: Math.round((selectedBarConfig?.iconScale ?? 1.0) * 100)
restoreMode: Binding.RestoreBinding
}
}
SettingsCard { SettingsCard {
iconName: "rounded_corner" iconName: "rounded_corner"
title: I18n.tr("Corners & Background") title: I18n.tr("Corners & Background")
settingKey: "barCorners" settingKey: "barCorners"
collapsible: true collapsible: true
expanded: false expanded: false
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled ?? false
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
@@ -1140,6 +1053,296 @@ Item {
} }
} }
SettingsToggleCard {
iconName: "fit_screen"
title: I18n.tr("Maximize Detection")
description: I18n.tr("Remove gaps and border when windows are maximized")
visible: (selectedBarConfig?.enabled ?? false) && (CompositorService.isNiri || CompositorService.isHyprland)
checked: selectedBarConfig?.maximizeDetection ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeDetection: checked
})
}
SettingsCard {
iconName: "filter_b_and_w"
title: I18n.tr("System Tray Icon Tint")
settingKey: "trayIconTint"
visible: selectedBarConfig?.enabled ?? false
StyledText {
text: I18n.tr("Choose monochrome or a theme color tint for system tray icons")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
SettingsButtonGroupRow {
text: I18n.tr("Mode")
model: [I18n.tr("None"), I18n.tr("Monochrome"), I18n.tr("Primary"), I18n.tr("Secondary")]
currentIndex: {
let mode = SettingsData.systemTrayIconTintMode || "none";
switch (mode) {
case "monochrome":
return 1;
case "primary":
return 2;
case "secondary":
return 3;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let mode = "none";
switch (index) {
case 1:
mode = "monochrome";
break;
case 2:
mode = "primary";
break;
case 3:
mode = "secondary";
break;
}
SettingsData.set("systemTrayIconTintMode", mode);
}
}
SettingsSliderRow {
id: trayTintSaturationSlider
text: I18n.tr("Tint Saturation")
description: I18n.tr("Controls how much original icon color is removed before applying tint")
visible: {
const mode = SettingsData.systemTrayIconTintMode || "none";
return mode === "primary" || mode === "secondary";
}
value: SettingsData.systemTrayIconTintSaturation ?? 50
minimum: 0
maximum: 100
unit: "%"
defaultValue: 50
onSliderDragFinished: finalValue => SettingsData.set("systemTrayIconTintSaturation", finalValue)
Binding {
target: trayTintSaturationSlider
property: "value"
value: SettingsData.systemTrayIconTintSaturation ?? 50
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: trayTintStrengthSlider
text: I18n.tr("Tint Strength")
description: I18n.tr("Controls how strongly the selected tint color is applied")
visible: {
const mode = SettingsData.systemTrayIconTintMode || "none";
return mode === "primary" || mode === "secondary";
}
value: SettingsData.systemTrayIconTintStrength ?? 135
minimum: 0
maximum: 200
unit: "%"
defaultValue: 135
onSliderDragFinished: finalValue => SettingsData.set("systemTrayIconTintStrength", finalValue)
Binding {
target: trayTintStrengthSlider
property: "value"
value: SettingsData.systemTrayIconTintStrength ?? 135
restoreMode: Binding.RestoreBinding
}
}
}
SettingsToggleCard {
iconName: "border_style"
title: I18n.tr("Border")
visible: selectedBarConfig?.enabled ?? false
checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked
})
SettingsButtonGroupRow {
text: I18n.tr("Color")
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
switch (selectedBarConfig?.borderColor || "surfaceText") {
case "surfaceText":
return 0;
case "secondary":
return 1;
case "primary":
return 2;
default:
return 0;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let newColor = "surfaceText";
switch (index) {
case 0:
newColor = "surfaceText";
break;
case 1:
newColor = "secondary";
break;
case 2:
newColor = "primary";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
borderColor: newColor
});
}
}
SettingsSliderRow {
id: borderOpacitySlider
text: I18n.tr("Opacity")
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
borderOpacity: finalValue / 100
});
}
Binding {
target: borderOpacitySlider
property: "value"
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: borderThicknessSlider
text: I18n.tr("Thickness")
value: selectedBarConfig?.borderThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
defaultValue: 1
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
borderThickness: finalValue
});
}
Binding {
target: borderThicknessSlider
property: "value"
value: selectedBarConfig?.borderThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
}
SettingsToggleCard {
iconName: "highlight"
title: I18n.tr("Widget Outline")
visible: selectedBarConfig?.enabled ?? false
checked: selectedBarConfig?.widgetOutlineEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineEnabled: checked
})
SettingsButtonGroupRow {
text: I18n.tr("Color")
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
switch (selectedBarConfig?.widgetOutlineColor || "primary") {
case "surfaceText":
return 0;
case "secondary":
return 1;
case "primary":
return 2;
default:
return 2;
}
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
let newColor = "primary";
switch (index) {
case 0:
newColor = "surfaceText";
break;
case 1:
newColor = "secondary";
break;
case 2:
newColor = "primary";
break;
}
SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineColor: newColor
});
}
}
SettingsSliderRow {
id: widgetOutlineOpacitySlider
text: I18n.tr("Opacity")
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineOpacity: finalValue / 100
});
}
Binding {
target: widgetOutlineOpacitySlider
property: "value"
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: widgetOutlineThicknessSlider
text: I18n.tr("Thickness")
value: selectedBarConfig?.widgetOutlineThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
defaultValue: 1
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineThickness: finalValue
});
}
Binding {
target: widgetOutlineThicknessSlider
property: "value"
value: selectedBarConfig?.widgetOutlineThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
}
SettingsCard { SettingsCard {
id: shadowCard id: shadowCard
iconName: "layers" iconName: "layers"
@@ -1147,7 +1350,7 @@ Item {
settingKey: "barShadow" settingKey: "barShadow"
collapsible: true collapsible: true
expanded: false expanded: false
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled ?? false
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "text") === "custom" readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "text") === "custom"
@@ -1269,114 +1472,62 @@ Item {
} }
SettingsToggleCard { SettingsToggleCard {
iconName: "border_style" iconName: "mouse"
title: I18n.tr("Border") title: I18n.tr("Scroll Wheel")
visible: selectedBarConfig?.enabled description: I18n.tr("Control workspaces and columns by scrolling on the bar")
checked: selectedBarConfig?.borderEnabled ?? false visible: selectedBarConfig?.enabled ?? false
checked: selectedBarConfig?.scrollEnabled ?? true
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked scrollEnabled: checked
}) })
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Color") text: I18n.tr("Y Axis")
model: ["Surface", "Secondary", "Primary"] model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
currentIndex: { currentIndex: {
switch (selectedBarConfig?.borderColor || "surfaceText") { switch (selectedBarConfig?.scrollYBehavior || "workspace") {
case "surfaceText": case "none":
return 0; return 0;
case "secondary": case "workspace":
return 1; return 1;
case "primary": case "column":
return 2; return 2;
default: default:
return 0; return 1;
} }
} }
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) if (!selected)
return; return;
let newColor = "surfaceText"; let behavior = "workspace";
switch (index) { switch (index) {
case 0: case 0:
newColor = "surfaceText"; behavior = "none";
break; break;
case 1: case 1:
newColor = "secondary"; behavior = "workspace";
break; break;
case 2: case 2:
newColor = "primary"; behavior = "column";
break; break;
} }
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
borderColor: newColor scrollYBehavior: behavior
}); });
} }
} }
SettingsSliderRow {
id: borderOpacitySlider
text: I18n.tr("Opacity")
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
borderOpacity: finalValue / 100
});
}
Binding {
target: borderOpacitySlider
property: "value"
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: borderThicknessSlider
text: I18n.tr("Thickness")
value: selectedBarConfig?.borderThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
defaultValue: 1
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
borderThickness: finalValue
});
}
Binding {
target: borderThicknessSlider
property: "value"
value: selectedBarConfig?.borderThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
}
SettingsToggleCard {
iconName: "highlight"
title: I18n.tr("Widget Outline")
visible: selectedBarConfig?.enabled
checked: selectedBarConfig?.widgetOutlineEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineEnabled: checked
})
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Color") text: I18n.tr("X Axis")
model: ["Surface", "Secondary", "Primary"] visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
currentIndex: { currentIndex: {
switch (selectedBarConfig?.widgetOutlineColor || "primary") { switch (selectedBarConfig?.scrollXBehavior || "column") {
case "surfaceText": case "none":
return 0; return 0;
case "secondary": case "workspace":
return 1; return 1;
case "primary": case "column":
return 2; return 2;
default: default:
return 2; return 2;
@@ -1385,67 +1536,23 @@ Item {
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) if (!selected)
return; return;
let newColor = "primary"; let behavior = "column";
switch (index) { switch (index) {
case 0: case 0:
newColor = "surfaceText"; behavior = "none";
break; break;
case 1: case 1:
newColor = "secondary"; behavior = "workspace";
break; break;
case 2: case 2:
newColor = "primary"; behavior = "column";
break; break;
} }
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineColor: newColor scrollXBehavior: behavior
}); });
} }
} }
SettingsSliderRow {
id: widgetOutlineOpacitySlider
text: I18n.tr("Opacity")
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineOpacity: finalValue / 100
});
}
Binding {
target: widgetOutlineOpacitySlider
property: "value"
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
restoreMode: Binding.RestoreBinding
}
}
SettingsSliderRow {
id: widgetOutlineThicknessSlider
text: I18n.tr("Thickness")
value: selectedBarConfig?.widgetOutlineThickness ?? 1
minimum: 1
maximum: 10
unit: "px"
defaultValue: 1
onSliderDragFinished: finalValue => {
SettingsData.updateBarConfig(selectedBarId, {
widgetOutlineThickness: finalValue
});
}
Binding {
target: widgetOutlineThicknessSlider
property: "value"
value: selectedBarConfig?.widgetOutlineThickness ?? 1
restoreMode: Binding.RestoreBinding
}
}
} }
} }
} }
+112
View File
@@ -0,0 +1,112 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
readonly property var muxTypeOptions: [
"tmux",
"zellij"
]
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "mux"
tags: ["mux", "multiplexer", "tmux", "zellij", "type"]
title: I18n.tr("Multiplexer")
iconName: "terminal"
SettingsDropdownRow {
tab: "mux"
tags: ["mux", "multiplexer", "tmux", "zellij", "type", "backend"]
settingKey: "muxType"
text: I18n.tr("Multiplexer Type")
description: I18n.tr("Terminal multiplexer backend to use")
options: root.muxTypeOptions
currentValue: SettingsData.muxType
onValueChanged: value => SettingsData.set("muxType", value)
}
}
SettingsCard {
tab: "mux"
tags: ["mux", "terminal", "custom", "command", "script"]
title: I18n.tr("Terminal")
iconName: "desktop_windows"
SettingsToggleRow {
tab: "mux"
tags: ["mux", "custom", "command", "override"]
settingKey: "muxUseCustomCommand"
text: I18n.tr("Use Custom Command")
description: I18n.tr("Override terminal with a custom command or script")
checked: SettingsData.muxUseCustomCommand
onToggled: checked => SettingsData.set("muxUseCustomCommand", checked)
}
Column {
width: parent?.width ?? 0
spacing: Theme.spacingS
visible: SettingsData.muxUseCustomCommand
StyledText {
width: parent.width
text: I18n.tr("The custom command used when attaching to sessions (receives the session name as the first argument)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
text: SettingsData.muxCustomCommand
placeholderText: I18n.tr("Enter command or script path")
onTextEdited: SettingsData.set("muxCustomCommand", text)
}
}
}
SettingsCard {
tab: "mux"
tags: ["mux", "session", "filter", "exclude", "hide"]
title: I18n.tr("Session Filter")
iconName: "filter_list"
Column {
width: parent?.width ?? 0
spacing: Theme.spacingS
StyledText {
width: parent.width
text: I18n.tr("Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankTextField {
width: parent.width
text: SettingsData.muxSessionFilter
placeholderText: I18n.tr("e.g., scratch, /^tmp_.*/, build")
onTextEdited: SettingsData.set("muxSessionFilter", text)
}
}
}
}
}
}
+30 -2
View File
@@ -1635,6 +1635,33 @@ Item {
onToggled: checked => SettingsData.set("blurEnabled", checked) onToggled: checked => SettingsData.set("blurEnabled", checked)
} }
SettingsToggleRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "contrast", "glass", "frosted"]
settingKey: "blurForegroundLayers"
text: I18n.tr("Foreground Layers")
description: I18n.tr("Show foreground surfaces on blurred panels for stronger contrast")
checked: SettingsData.blurForegroundLayers ?? true
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurForegroundLayers", checked)
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "foreground", "layers", "outline", "border", "cards", "widgets", "notifications", "control center"]
settingKey: "blurLayerOutlineOpacity"
text: I18n.tr("Layer Outline Opacity")
description: I18n.tr("Controls outlines around blurred foreground cards, pills, and notification cards")
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
minimum: 0
maximum: 40
unit: "%"
defaultValue: 12
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
}
SettingsDropdownRow { SettingsDropdownRow {
tab: "theme" tab: "theme"
tags: ["blur", "border", "outline", "edge"] tags: ["blur", "border", "outline", "edge"]
@@ -1678,12 +1705,13 @@ Item {
tags: ["blur", "border", "opacity"] tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity" settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity") text: I18n.tr("Blur Border Opacity")
description: I18n.tr("Controls the outer edge of protocol-blurred windows")
visible: SettingsData.blurEnabled visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 1.0) * 100) value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "%" unit: "%"
defaultValue: 100 defaultValue: 35
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100) onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
} }
} }
@@ -14,6 +14,10 @@ FloatingWindow {
property int selectedIndex: -1 property int selectedIndex: -1
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property var parentModal: null property var parentModal: null
readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.36 : 0.78) : 1.0
readonly property real fieldAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.18 : 0.62) : 1.0
readonly property real rowAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.12 : 0.52) : 0.30
signal widgetSelected(string widgetId, string targetSection) signal widgetSelected(string widgetId, string targetSection)
@@ -94,7 +98,7 @@ FloatingWindow {
minimumSize: Qt.size(400, 350) minimumSize: Qt.size(400, 350)
implicitWidth: 500 implicitWidth: 500
implicitHeight: 550 implicitHeight: 550
color: Theme.surfaceContainer color: blurActive ? "transparent" : Theme.surfaceContainer
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -119,6 +123,24 @@ FloatingWindow {
}); });
} }
WindowBlur {
targetWindow: root
blurX: 0
blurY: 0
blurWidth: root.visible ? root.width : 0
blurHeight: root.visible ? root.height : 0
blurRadius: Theme.cornerRadius
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, root.surfaceAlpha)
border.color: root.blurActive ? Theme.outlineMedium : "transparent"
border.width: root.blurActive ? Theme.layerOutlineWidth : 0
antialiasing: true
}
FocusScope { FocusScope {
id: widgetKeyHandler id: widgetKeyHandler
@@ -184,8 +206,7 @@ FloatingWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainerHigh, root.blurActive ? 0.20 : 0.50)
opacity: 0.5
} }
Row { Row {
@@ -258,7 +279,7 @@ FloatingWindow {
width: parent.width width: parent.width
height: 48 height: 48
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, root.fieldAlpha)
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary
leftIconName: "search" leftIconName: "search"
@@ -302,9 +323,10 @@ FloatingWindow {
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
color: isSelected ? Theme.primarySelected : widgetArea.containsMouse ? Theme.primaryHover : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: isSelected ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.22 : 0.16) : widgetArea.containsMouse ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.14 : 0.08) : Theme.withAlpha(Theme.surfaceVariant, root.rowAlpha)
border.color: isSelected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: isSelected ? Theme.primary : Theme.outlineMedium
border.width: isSelected ? 2 : 1 border.width: isSelected ? 2 : Theme.layerOutlineWidth
antialiasing: true
Row { Row {
anchors.fill: parent anchors.fill: parent
+2 -2
View File
@@ -90,9 +90,9 @@ PanelWindow {
case ToastService.levelWarn: case ToastService.levelWarn:
return Theme.warning; return Theme.warning;
case ToastService.levelInfo: case ToastService.levelInfo:
return Theme.surfaceContainer; return Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency);
default: default:
return Theme.surfaceContainer; return Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency);
} }
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -5,6 +5,7 @@ import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
import qs.Services import qs.Services
import qs.Widgets
Scope { Scope {
id: niriOverviewScope id: niriOverviewScope
@@ -67,6 +68,20 @@ Scope {
hideSpotlight(); hideSpotlight();
} }
onIsClosingChanged: {
if (!isClosing) {
closeTimer.stop();
return;
}
closeTimer.restart();
}
Timer {
id: closeTimer
interval: Theme.expressiveDurations.fast
onTriggered: niriOverviewScope.resetState()
}
Loader { Loader {
id: niriOverlayLoader id: niriOverlayLoader
active: overlayActive || isClosing active: overlayActive || isClosing
@@ -124,6 +139,17 @@ Scope {
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
} }
WindowBlur {
targetWindow: overlayWindow
readonly property real s: Math.min(1, spotlightContainer.scale)
readonly property bool active: overlayWindow.shouldShowSpotlight && spotlightContainer.opacity > 0
blurX: spotlightContainer.x + spotlightContainer.width * (1 - s) * 0.5
blurY: spotlightContainer.y + spotlightContainer.height * (1 - s) * 0.5
blurWidth: active ? spotlightContainer.width * s : 0
blurHeight: active ? spotlightContainer.height * s : 0
blurRadius: Theme.cornerRadius
}
onShouldShowSpotlightChanged: { onShouldShowSpotlightChanged: {
if (shouldShowSpotlight) { if (shouldShowSpotlight) {
if (launcherContent?.controller) { if (launcherContent?.controller) {
@@ -244,16 +270,10 @@ Scope {
layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0) layer.textureSize: layer.enabled ? Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr)) : Qt.size(0, 0)
Behavior on scale { Behavior on scale {
id: scaleAnimation
NumberAnimation { NumberAnimation {
duration: Theme.expressiveDurations.fast duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
niriOverviewScope.resetState();
}
} }
} }
+32 -1
View File
@@ -71,6 +71,36 @@ Singleton {
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name)); return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
} }
// Resolve a PwNode by name from the live typed list and assign it as the
// default sink. Going through Pipewire.nodes.values directly (no .filter
// / spread / .sort / property var) avoids QML type erasure to QObject*,
// which newer quickshell rejects when assigning to preferredDefaultAudioSink.
function setDefaultSinkByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSink = node;
return true;
}
}
return false;
}
function setDefaultSourceByName(name) {
if (!name)
return false;
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i];
if (node && node.name === name && node.audio && !node.isSink && !node.isStream) {
Pipewire.preferredDefaultAudioSource = node;
return true;
}
}
return false;
}
function cycleAudioOutput() { function cycleAudioOutput() {
const sinks = getAvailableSinks(); const sinks = getAvailableSinks();
if (sinks.length < 2) if (sinks.length < 2)
@@ -80,7 +110,8 @@ Singleton {
const currentIndex = sinks.findIndex(s => s.name === currentName); const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length; const nextIndex = (currentIndex + 1) % sinks.length;
const nextSink = sinks[nextIndex]; const nextSink = sinks[nextIndex];
Pipewire.preferredDefaultAudioSink = nextSink; if (!setDefaultSinkByName(nextSink.name))
Pipewire.preferredDefaultAudioSink = nextSink;
const name = displayName(nextSink); const name = displayName(nextSink);
audioOutputCycled(name, sinkIcon(nextSink)); audioOutputCycled(name, sinkIcon(nextSink));
return name; return name;

Some files were not shown because too many files have changed in this diff Show More