1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-03 03:59:11 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
purian23 3420d2be93 add(systemd): dms-tray-watcher service & commands for early tray registration 2026-07-06 14:52:36 -04:00
100 changed files with 1910 additions and 2660 deletions
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=DMS Status Notifier Watcher (early tray)
# The tray watcher starts in parallel with the compositor and owns org.kde.StatusNotifierWatcher before
# graphical-session.target, i.e. before any XDG autostart app launches.
PartOf=graphical-session.target
Before=graphical-session.target
[Service]
Type=dbus
BusName=org.kde.StatusNotifierWatcher
ExecStart=/usr/bin/dms tray-watcher
Restart=on-failure
RestartSec=1
Slice=session.slice
[Install]
WantedBy=graphical-session.target
+2 -5
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control. Command-line interface and daemon for shell management and system control.
**dankinstall** **dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
## System Integration ## System Integration
@@ -193,7 +193,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
## Supported Distributions ## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives) Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
**Arch Linux** **Arch Linux**
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency. Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
@@ -214,7 +214,4 @@ Most packages available in standard repos. Minimal building required.
**Gentoo** **Gentoo**
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration. Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
**Void Linux**
Uses XBPS with the DMS and DankLinux self-hosted repositories.
See installer output for distribution-specific details during installation. See installer output for distribution-specific details during installation.
+1
View File
@@ -774,5 +774,6 @@ func getCommonCommands() []*cobra.Command {
trashCmd, trashCmd,
systemCmd, systemCmd,
switchUserCmd, switchUserCmd,
trayWatcherCmd,
} }
} }
+1 -4
View File
@@ -1534,8 +1534,6 @@ func packageInstallHint() string {
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)" return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
case distros.FamilyArch: case distros.FamilyArch:
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'" return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
case distros.FamilyVoid:
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf)"
default: default:
return "Run 'dms greeter install' to install greeter" return "Run 'dms greeter install' to install greeter"
} }
@@ -1574,8 +1572,7 @@ func isPackageOnlyGreeterDistro() bool {
config.Family == distros.FamilySUSE || config.Family == distros.FamilySUSE ||
config.Family == distros.FamilyUbuntu || config.Family == distros.FamilyUbuntu ||
config.Family == distros.FamilyFedora || config.Family == distros.FamilyFedora ||
config.Family == distros.FamilyArch || config.Family == distros.FamilyArch
config.Family == distros.FamilyVoid
} }
func promptCompositorChoice(compositors []string) (string, error) { func promptCompositorChoice(compositors []string) (string, error) {
-13
View File
@@ -10,7 +10,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"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/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/privesc"
@@ -299,9 +298,6 @@ func runSetup() error {
if wmSelected { if wmSelected {
if wm == deps.WindowManagerMango { if wm == deps.WindowManagerMango {
useSystemd = false useSystemd = false
} else if isVoidSetup() {
useSystemd = false
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
} else { } else {
useSystemd = promptSystemd() useSystemd = promptSystemd()
} }
@@ -376,15 +372,6 @@ func runSetup() error {
return nil return nil
} }
func isVoidSetup() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
// Add user to the input group for the evdev manager for inut state tracking. // Add user to the input group for the evdev manager for inut state tracking.
// Caps Lock OSD and the Caps Lock bar indicator. // Caps Lock OSD and the Caps Lock bar indicator.
func ensureInputGroup() { func ensureInputGroup() {
+24
View File
@@ -0,0 +1,24 @@
package main
import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/traywatcher"
"github.com/spf13/cobra"
)
var trayWatcherCmd = &cobra.Command{
Use: "tray-watcher",
Short: "Run a minimal StatusNotifierWatcher for early tray registration",
Long: `Run a minimal org.kde.StatusNotifierWatcher daemon.
Started early in the session (Before=graphical-session.target via
dms-tray-watcher.service), it lets tray apps launched by XDG autostart
register their items before the shell finishes loading, and keeps them
registered across shell restarts. The shell's tray host picks items up from
this watcher; if it is not running, the shell's built-in watcher takes over.`,
Run: func(cmd *cobra.Command, args []string) {
if err := traywatcher.Run(); err != nil {
log.Fatalf("%v", err)
}
},
}
+3 -6
View File
@@ -61,10 +61,6 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true) return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
} }
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
}
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
@@ -884,8 +880,9 @@ func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalComma
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars) config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
spawnDms := `spawn-at-startup "dms" "run"` // Watcher spawns first so it owns the SNI name before dms/tray apps start.
if !strings.Contains(config, spawnDms) { spawnDms := "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\""
if !strings.Contains(config, `spawn-at-startup "dms" "run"`) {
// Insert spawn-at-startup for dms after the environment block // Insert spawn-at-startup for dms after the environment block
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`) envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
if loc := envBlockEnd.FindStringIndex(config); loc != nil { if loc := envBlockEnd.FindStringIndex(config); loc != nil {
+12
View File
@@ -520,9 +520,21 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandLuaConfig, "input =") assert.Contains(t, HyprlandLuaConfig, "input =")
} }
// In non-systemd mode dms is launched from the compositor config, so the tray
// watcher must be launched there too, before dms, to own the SNI name first.
func TestNonSystemdLaunchesTrayWatcherBeforeShell(t *testing.T) {
hypr := transformHyprlandLuaForNonSystemd(HyprlandLuaConfig, "ghostty")
assert.Contains(t, hypr, "hl.exec_cmd(\"dms tray-watcher\")\n\thl.exec_cmd(\"dms run\")")
niri := (&ConfigDeployer{}).transformNiriConfigForNonSystemd(NiriConfig, "ghostty")
assert.Contains(t, niri, "spawn-at-startup \"dms\" \"tray-watcher\"\nspawn-at-startup \"dms\" \"run\"")
}
func TestMangoConfigStructure(t *testing.T) { func TestMangoConfigStructure(t *testing.T) {
assert.Contains(t, MangoConfig, "exec-once=dms run") assert.Contains(t, MangoConfig, "exec-once=dms run")
assert.NotContains(t, MangoConfig, "exec_once=dms run") assert.NotContains(t, MangoConfig, "exec_once=dms run")
// Tray watcher must start before the shell so it owns the SNI name first.
assert.Contains(t, MangoConfig, "exec-once=dms tray-watcher\nexec-once=dms run")
assert.Contains(t, MangoConfig, "source=./dms/binds.conf") assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left") assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down") assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
+2 -2
View File
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
copy-on-select = false copy-on-select = false
confirm-close-surface = false confirm-close-surface = false
# Disable in-app Ghostty toast notifications # Disable annoying copied to clipboard
app-notifications = false app-notifications = no-clipboard-copy,no-config-reload
# Key bindings for common actions # Key bindings for common actions
#keybind = ctrl+c=copy_to_clipboard #keybind = ctrl+c=copy_to_clipboard
+1 -1
View File
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle")) hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle")) hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper")) hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview")) hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle")) hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
# Notepad # Notepad
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
# Browse Wallpapers # Browse Wallpapers
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
# Power Menu # Power Menu
bind=SUPER,x,spawn,dms ipc call powermenu toggle bind=SUPER,x,spawn,dms ipc call powermenu toggle
# Cycle Display Profile # Cycle Display Profile
+3 -1
View File
@@ -7,7 +7,9 @@ env=XDG_SESSION_TYPE,wayland
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs # exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
# every exec= on each config reload, and DMS reloads the config, which would # every exec= on each config reload, and DMS reloads the config, which would
# spawn a new shell on every reload. # spawn a new shell on every reload. The tray watcher starts first so it owns
# the SNI name before dms and the tray apps come up.
exec-once=dms tray-watcher
exec-once=dms run exec-once=dms run
source=./dms/colors.conf source=./dms/colors.conf
+1 -1
View File
@@ -24,7 +24,7 @@ binds {
spawn "dms" "ipc" "call" "settings" "focusOrToggle"; spawn "dms" "ipc" "call" "settings" "focusOrToggle";
} }
Mod+Y hotkey-overlay-title="Browse Wallpapers" { Mod+Y hotkey-overlay-title="Browse Wallpapers" {
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper"; spawn "dms" "ipc" "call" "dankdash" "wallpaper";
} }
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; } Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; } Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
@@ -20,10 +20,3 @@ window-rule {
tiled-state true tiled-state true
draw-border-with-background false draw-border-with-background false
} }
layer-rule {
exclude namespace="^dms:bar$"
background-effect {
xray false
}
}
+6 -6
View File
@@ -265,9 +265,9 @@ recent-windows {
} }
// Include dms files // Include dms files
include optional=true "dms/colors.kdl" include "dms/colors.kdl"
include optional=true "dms/layout.kdl" include "dms/layout.kdl"
include optional=true "dms/alttab.kdl" include "dms/alttab.kdl"
include optional=true "dms/binds.kdl" include "dms/binds.kdl"
include optional=true "dms/outputs.kdl" include "dms/outputs.kdl"
include optional=true "dms/cursor.kdl" include "dms/cursor.kdl"
+1
View File
@@ -116,6 +116,7 @@ func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" + `hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" + fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
`hl.on("hyprland.start", function()` + "\n" + `hl.on("hyprland.start", function()` + "\n" +
` hl.exec_cmd("dms tray-watcher")` + "\n" +
` hl.exec_cmd("dms run")` + "\n" + ` hl.exec_cmd("dms run")` + "\n" +
`end)` + "\n" + `end)` + "\n" +
hyprlandStartupEnd hyprlandStartupEnd
+4 -2
View File
@@ -587,13 +587,15 @@ TERMINAL=%s
} }
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error { func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
// Pull in the tray watcher alongside dms; its Before=graphical-session.target
// ordering makes it claim the SNI name before autostart apps start.
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil { if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms", "dms-tray-watcher").Run(); err != nil {
b.log("Warning: failed to add dms as a want for niri.service") b.log("Warning: failed to add dms as a want for niri.service")
} }
case deps.WindowManagerHyprland: case deps.WindowManagerHyprland:
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil { if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms", "dms-tray-watcher").Run(); err != nil {
b.log("Warning: failed to add dms as a want for hyprland-session.target") b.log("Warning: failed to add dms as a want for hyprland-session.target")
} }
} }
-3
View File
@@ -17,7 +17,6 @@ const (
FamilyDebian DistroFamily = "debian" FamilyDebian DistroFamily = "debian"
FamilyNix DistroFamily = "nix" FamilyNix DistroFamily = "nix"
FamilyGentoo DistroFamily = "gentoo" FamilyGentoo DistroFamily = "gentoo"
FamilyVoid DistroFamily = "void"
) )
// PackageManagerType defines the package manager a distro uses // PackageManagerType defines the package manager a distro uses
@@ -30,7 +29,6 @@ const (
PackageManagerZypper PackageManagerType = "zypper" PackageManagerZypper PackageManagerType = "zypper"
PackageManagerNix PackageManagerType = "nix" PackageManagerNix PackageManagerType = "nix"
PackageManagerPortage PackageManagerType = "portage" PackageManagerPortage PackageManagerType = "portage"
PackageManagerXBPS PackageManagerType = "xbps"
) )
// RepositoryType defines the type of repository for a package // RepositoryType defines the type of repository for a package
@@ -44,7 +42,6 @@ const (
RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE) RepoTypeOBS RepositoryType = "obs" // OpenBuild Service (Debian/OpenSUSE)
RepoTypeFlake RepositoryType = "flake" // Nix flake RepoTypeFlake RepositoryType = "flake" // Nix flake
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
RepoTypeXBPS RepositoryType = "xbps" // Custom XBPS repository
RepoTypeManual RepositoryType = "manual" // Manual build from source RepoTypeManual RepositoryType = "manual" // Manual build from source
) )
-530
View File
@@ -1,530 +0,0 @@
package distros
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
)
const (
VoidDMSRepo = "https://avengemedia.github.io/DankMaterialShell/current"
VoidDankLinuxRepo = "https://avengemedia.github.io/DankLinux/current"
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
voidRunitSvDir = "/etc/sv"
voidRunitServiceDir = "/var/service"
)
func init() {
Register("void", "#478061", FamilyVoid, func(config DistroConfig, logChan chan<- string) Distribution {
return NewVoidDistribution(config, logChan)
})
}
type VoidDistribution struct {
*BaseDistribution
config DistroConfig
}
func NewVoidDistribution(config DistroConfig, logChan chan<- string) *VoidDistribution {
return &VoidDistribution{
BaseDistribution: NewBaseDistribution(logChan),
config: config,
}
}
func (v *VoidDistribution) GetID() string {
return v.config.ID
}
func (v *VoidDistribution) GetColorHex() string {
return v.config.ColorHex
}
func (v *VoidDistribution) GetFamily() DistroFamily {
return v.config.Family
}
func (v *VoidDistribution) GetPackageManager() PackageManagerType {
return PackageManagerXBPS
}
func (v *VoidDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
return v.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
}
func (v *VoidDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
var dependencies []deps.Dependency
dependencies = append(dependencies, v.detectDMS())
dependencies = append(dependencies, v.detectSpecificTerminal(terminal))
dependencies = append(dependencies, v.detectGit())
dependencies = append(dependencies, v.detectWindowManager(wm))
dependencies = append(dependencies, v.detectQuickshell())
dependencies = append(dependencies, v.detectDMSGreeter())
dependencies = append(dependencies, v.detectXDGPortal())
dependencies = append(dependencies, v.detectAccountsService())
dependencies = append(dependencies, v.detectDBus())
dependencies = append(dependencies, v.detectElogind())
if wm == deps.WindowManagerHyprland {
dependencies = append(dependencies, v.detectHyprlandTools()...)
}
if wm == deps.WindowManagerNiri || wm == deps.WindowManagerMango {
dependencies = append(dependencies, v.detectXwaylandSatellite())
}
dependencies = append(dependencies, v.detectMatugen())
dependencies = append(dependencies, v.detectDgop())
return dependencies, nil
}
func (v *VoidDistribution) detectDMS() deps.Dependency {
status := deps.StatusMissing
version := ""
variant := deps.VariantStable
if v.packageInstalled("dms-git") {
status = deps.StatusInstalled
version = v.packageVersion("dms-git")
variant = deps.VariantGit
} else if v.packageInstalled("dms") {
status = deps.StatusInstalled
version = v.packageVersion("dms")
} else if v.commandExists("dms") {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: "dms (DankMaterialShell)",
Status: status,
Version: version,
Description: "Desktop Management System package",
Required: true,
Variant: variant,
CanToggle: true,
}
}
func (v *VoidDistribution) detectQuickshell() deps.Dependency {
dep := v.BaseDistribution.detectQuickshell()
dep.CanToggle = false
return dep
}
func (v *VoidDistribution) detectXDGPortal() deps.Dependency {
return v.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", v.packageInstalled("xdg-desktop-portal-gtk"))
}
func (v *VoidDistribution) detectDMSGreeter() deps.Dependency {
return v.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", v.packageInstalled("dms-greeter"))
}
func (v *VoidDistribution) detectAccountsService() deps.Dependency {
return v.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", v.packageInstalled("accountsservice"))
}
func (v *VoidDistribution) detectDBus() deps.Dependency {
return v.detectPackage("dbus", "D-Bus system and session message bus", v.packageInstalled("dbus"))
}
func (v *VoidDistribution) detectElogind() deps.Dependency {
return v.detectPackage("elogind", "loginctl/logind provider for power management and session tracking", v.packageInstalled("elogind") || v.commandExists("loginctl"))
}
func (v *VoidDistribution) detectXwaylandSatellite() deps.Dependency {
return v.detectPackage("xwayland-satellite", "Xwayland support", v.packageInstalled("xwayland-satellite"))
}
func (v *VoidDistribution) packageInstalled(pkg string) bool {
return exec.Command("xbps-query", pkg).Run() == nil
}
func (v *VoidDistribution) packageVersion(pkg string) string {
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func (v *VoidDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return v.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
}
func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
packages := map[string]PackageMapping{
"git": {Name: "git", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
"dbus": {Name: "dbus", Repository: RepoTypeSystem},
"elogind": {Name: "elogind", Repository: RepoTypeSystem},
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
"dms (DankMaterialShell)": v.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo},
"dgop": {Name: "dgop", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
}
switch wm {
case deps.WindowManagerHyprland:
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeXBPS, RepoURL: VoidHyprlandRepo}
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
case deps.WindowManagerNiri:
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
case deps.WindowManagerMango:
packages["mango"] = PackageMapping{Name: "mangowc", Repository: RepoTypeSystem}
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
}
return packages
}
func (v *VoidDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
if variant == deps.VariantStable {
return PackageMapping{Name: "dms", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
return PackageMapping{Name: "dms-git", Repository: RepoTypeXBPS, RepoURL: VoidDMSRepo}
}
func (v *VoidDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.06,
Step: "Checking XBPS...",
IsComplete: false,
LogOutput: "Checking for xbps-install",
}
if _, err := exec.LookPath("xbps-install"); err != nil {
return fmt.Errorf("xbps-install not found; Void Linux package tools are required: %w", err)
}
return nil
}
func (v *VoidDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, disabledFlags map[string]bool, skipGlobalUseFlags bool, progressChan chan<- InstallProgressMsg) error {
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.05,
Step: "Checking system prerequisites...",
IsComplete: false,
LogOutput: "Starting prerequisite check...",
}
if wm == deps.WindowManagerHyprland {
arch, err := v.xbpsArch(ctx)
if err != nil {
return fmt.Errorf("failed to detect XBPS architecture for Hyprland repository selection: %w", err)
}
if arch != "x86_64" {
return fmt.Errorf("hyprland on Void Linux is installed from %s, which currently provides x86_64 packages only (detected %s)", VoidHyprlandRepo, arch)
}
}
if err := v.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err)
}
systemPkgs, xbpsPkgs := v.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if len(xbpsPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.15,
Step: "Enabling DMS XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
LogOutput: "Setting up custom XBPS repositories for DMS packages",
}
if err := v.enableXBPSRepos(ctx, xbpsPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable XBPS repositories: %w", err)
}
}
allPkgs := v.uniquePackageNames(systemPkgs, v.extractPackageNames(xbpsPkgs))
if len(allPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.35,
Step: fmt.Sprintf("Installing %d XBPS packages...", len(allPkgs)),
IsComplete: false,
NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing XBPS packages: %s", strings.Join(allPkgs, ", ")),
}
if err := v.installXBPSPackages(ctx, allPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install XBPS packages: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.90,
Step: "Configuring system...",
IsComplete: false,
LogOutput: "Starting post-installation configuration...",
}
v.log("Void Linux detected; DMS environment and autostart will be configured in the compositor config instead of systemd")
if err := v.ensureSessionServices(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to enable Void session services: %w", err)
}
progressChan <- InstallProgressMsg{
Phase: PhaseComplete,
Progress: 1.0,
Step: "Installation complete!",
IsComplete: true,
LogOutput: "All packages installed and configured successfully",
}
return nil
}
func (v *VoidDistribution) ensureSessionServices(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if !v.isRunitSystem() {
v.log("Void runit service directory not detected; skipping dbus/elogind service enablement")
return nil
}
for _, service := range []string{"dbus", "elogind"} {
if !v.runitServiceInstalled(service) {
v.log(fmt.Sprintf("Warning: %s runit service not found in %s; power/session actions may not work until %s is installed", service, voidRunitSvDir, service))
continue
}
if v.runitServiceEnabled(service) {
v.log(fmt.Sprintf("Void runit service %s already enabled", service))
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseConfiguration,
Progress: 0.92,
Step: fmt.Sprintf("Enabling %s runit service...", service),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)),
LogOutput: fmt.Sprintf("Enabling Void runit service: %s", service),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("ln -sf %s %s", filepath.Join(voidRunitSvDir, service), filepath.Join(voidRunitServiceDir, service)))
if err := v.runWithProgress(cmd, progressChan, PhaseConfiguration, 0.92, 0.95); err != nil {
return fmt.Errorf("failed to enable %s runit service: %w", service, err)
}
v.log(fmt.Sprintf("✓ Enabled %s runit service", service))
}
return nil
}
func (v *VoidDistribution) isRunitSystem() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(voidRunitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func (v *VoidDistribution) runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(voidRunitSvDir, name))
return err == nil && fi.IsDir()
}
func (v *VoidDistribution) runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(voidRunitServiceDir, name))
return err == nil
}
func (v *VoidDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool, disabledFlags map[string]bool) ([]string, []PackageMapping) {
systemPkgs := []string{}
xbpsPkgs := []PackageMapping{}
variantMap := make(map[string]deps.PackageVariant)
for _, dep := range dependencies {
variantMap[dep.Name] = dep.Variant
}
packageMap := v.GetPackageMappingWithVariants(wm, variantMap)
for _, dep := range dependencies {
if disabledFlags[dep.Name] {
continue
}
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
continue
}
pkgInfo, exists := packageMap[dep.Name]
if !exists {
v.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
continue
}
switch pkgInfo.Repository {
case RepoTypeXBPS:
xbpsPkgs = append(xbpsPkgs, pkgInfo)
case RepoTypeSystem:
systemPkgs = append(systemPkgs, pkgInfo.Name)
}
}
return systemPkgs, xbpsPkgs
}
func (v *VoidDistribution) enableXBPSRepos(ctx context.Context, xbpsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool)
enabledRepoURLs := []string{}
for _, pkg := range xbpsPkgs {
if pkg.RepoURL == "" || enabledRepos[pkg.RepoURL] {
continue
}
repoName := v.xbpsRepoName(pkg.RepoURL)
confPath := filepath.Join("/etc/xbps.d", repoName+".conf")
repoLine := fmt.Sprintf("repository=%s", pkg.RepoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
v.log(fmt.Sprintf("XBPS repo %s already configured, skipping", pkg.RepoURL))
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
continue
}
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.18,
Step: fmt.Sprintf("Adding XBPS repo %s...", repoName),
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("echo 'repository=%s' | sudo tee %s", pkg.RepoURL, confPath),
LogOutput: fmt.Sprintf("Adding XBPS repository: %s", pkg.RepoURL),
}
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/xbps.d")
if err := v.runWithProgress(mkdirCmd, progressChan, PhaseSystemPackages, 0.18, 0.19); err != nil {
return fmt.Errorf("failed to create /etc/xbps.d: %w", err)
}
writeCmd := privesc.ExecCommand(ctx, sudoPassword,
fmt.Sprintf("bash -c 'printf \"%%s\\n\" %q > %s'", repoLine, confPath))
if err := v.runWithProgress(writeCmd, progressChan, PhaseSystemPackages, 0.19, 0.22); err != nil {
return fmt.Errorf("failed to add XBPS repo %s: %w", pkg.RepoURL, err)
}
enabledRepos[pkg.RepoURL] = true
enabledRepoURLs = append(enabledRepoURLs, pkg.RepoURL)
}
if len(enabledRepos) > 0 {
syncArgs := []string{"xbps-install", "-Sy", "-i"}
for _, repoURL := range enabledRepoURLs {
syncArgs = append(syncArgs, "--repository", repoURL)
}
syncCommand := strings.Join(syncArgs, " ")
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.25,
Step: "Synchronizing XBPS repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo sh -c 'yes y | " + syncCommand + "'",
LogOutput: "Synchronizing XBPS repository indexes",
}
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | "+syncCommand+"'")
if err := v.runWithProgress(syncCmd, progressChan, PhaseSystemPackages, 0.25, 0.30); err != nil {
return fmt.Errorf("failed to synchronize XBPS repositories: %w", err)
}
}
return nil
}
func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
switch repoURL {
case VoidDMSRepo:
return "dms"
case VoidDankLinuxRepo:
return "danklinux"
case VoidHyprlandRepo:
return "hyprland"
default:
name := strings.TrimPrefix(repoURL, "https://")
name = strings.TrimPrefix(name, "http://")
name = strings.NewReplacer("/", "-", ".", "-").Replace(name)
return name
}
}
func (v *VoidDistribution) xbpsArch(ctx context.Context) (string, error) {
output, err := exec.CommandContext(ctx, "xbps-uhelper", "arch").Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}
func (v *VoidDistribution) installXBPSPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 {
return nil
}
args := append([]string{"xbps-install", "-Sy"}, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing XBPS packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
return v.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.85)
}
func (v *VoidDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages))
for i, pkg := range packages {
names[i] = pkg.Name
}
return names
}
func (v *VoidDistribution) uniquePackageNames(groups ...[]string) []string {
seen := make(map[string]bool)
var unique []string
for _, group := range groups {
for _, pkg := range group {
if pkg == "" || seen[pkg] {
continue
}
seen[pkg] = true
unique = append(unique, pkg)
}
}
return unique
}
-150
View File
@@ -30,11 +30,6 @@ const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const GreeterCacheDir = "/var/cache/dms-greeter" const GreeterCacheDir = "/var/cache/dms-greeter"
const (
runitSvDir = "/etc/sv"
runitServiceDir = "/var/service"
)
func DetectDMSPath() (string, error) { func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() return config.LocateDMSConfig()
} }
@@ -46,96 +41,6 @@ func IsNixOS() bool {
return err == nil return err == nil
} }
func IsVoidLinux() bool {
osInfo, err := distros.GetOSInfo()
if err != nil {
return false
}
config, exists := distros.Registry[osInfo.Distribution.ID]
return exists && config.Family == distros.FamilyVoid
}
func isRunit() bool {
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
return true
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
return true
}
return false
}
func runitServiceInstalled(name string) bool {
fi, err := os.Stat(filepath.Join(runitSvDir, name))
return err == nil && fi.IsDir()
}
func runitServiceEnabled(name string) bool {
_, err := os.Lstat(filepath.Join(runitServiceDir, name))
return err == nil
}
func enableRunitService(name, sudoPassword string) error {
if !runitServiceInstalled(name) {
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
}
if runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "ln", "-sf",
filepath.Join(runitSvDir, name), filepath.Join(runitServiceDir, name))
}
func disableRunitService(name, sudoPassword string) error {
if !runitServiceEnabled(name) {
return nil
}
return privesc.Run(context.Background(), sudoPassword, "rm", "-f",
filepath.Join(runitServiceDir, name))
}
func ensureRunitSeat(greeterUser, sudoPassword string, logFunc func(string)) {
if runitServiceInstalled("seatd") {
if err := enableRunitService("seatd", sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ could not enable seatd: %v", err))
} else {
logFunc("✓ seatd enabled")
}
} else {
logFunc("⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
}
if err := privesc.Run(context.Background(), sudoPassword, "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
logFunc(fmt.Sprintf("⚠ could not add %s to seat groups: %v", greeterUser, err))
} else {
logFunc(fmt.Sprintf("✓ %s added to seat groups (_seatd, video, input)", greeterUser))
}
}
func ensureGreetdPamRundir(sudoPassword string, logFunc func(string)) {
const pamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(pamPath)
if err != nil {
logFunc(fmt.Sprintf("⚠ could not read %s: %v", pamPath, err))
return
}
if strings.Contains(string(data), "pam_rundir") {
logFunc("✓ pam_rundir already present in greetd PAM")
return
}
line := "session optional pam_rundir.so"
if err := privesc.Run(context.Background(), sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
logFunc(fmt.Sprintf("⚠ could not add pam_rundir to %s: %v", pamPath, err))
return
}
logFunc("✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
}
func DetectGreeterGroup() string { func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group") data, err := os.ReadFile("/etc/group")
if err != nil { if err != nil {
@@ -861,8 +766,6 @@ func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd") installCmd = privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y greetd")
case distros.FamilyGentoo: case distros.FamilyGentoo:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd") installCmd = privesc.ExecCommand(ctx, sudoPassword, "emerge --ask n sys-apps/greetd")
case distros.FamilyVoid:
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy 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:
@@ -989,14 +892,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
} }
failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper) failHint = fmt.Sprintf("⚠ dms-greeter install failed. Install from AUR: %s -S greetd-dms-greeter-git", aurHelper)
installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git") installCmd = exec.CommandContext(ctx, aurHelper, "-S", "--noconfirm", "greetd-dms-greeter-git")
case distros.FamilyVoid:
failHint = "⚠ dms-greeter install failed. Add the DMS XBPS repo manually:\necho 'repository=https://avengemedia.github.io/DankMaterialShell/current' | sudo tee /etc/xbps.d/dms.conf\nsudo xbps-install -Sy dms-greeter"
logFunc("Adding DMS XBPS repository...")
if err := ensureVoidXBPSRepo(ctx, sudoPassword, "dms", distros.VoidDMSRepo); err != nil {
logFunc(fmt.Sprintf("⚠ Failed to add DMS XBPS repository: %v", err))
}
privesc.ExecCommand(ctx, sudoPassword, "sh -c 'yes y | xbps-install -Sy -i --repository "+distros.VoidDMSRepo+"'").Run()
installCmd = privesc.ExecCommand(ctx, sudoPassword, "xbps-install -Sy dms-greeter")
default: default:
return false return false
} }
@@ -1014,20 +909,6 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
return true return true
} }
func ensureVoidXBPSRepo(ctx context.Context, sudoPassword, name, repoURL string) error {
confPath := filepath.Join("/etc/xbps.d", name+".conf")
repoLine := fmt.Sprintf("repository=%s", repoURL)
repoFileContent := repoLine + "\n"
if content, err := os.ReadFile(confPath); err == nil && string(content) == repoFileContent {
return nil
}
if err := privesc.Run(ctx, sudoPassword, "mkdir", "-p", "/etc/xbps.d"); err != nil {
return err
}
return privesc.Run(ctx, sudoPassword, "sh", "-c",
fmt.Sprintf("printf '%%s\\n' %q > %s", repoLine, confPath))
}
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory // CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if IsGreeterPackaged() { if IsGreeterPackaged() {
@@ -2394,19 +2275,6 @@ func checkSystemdEnabled(service string) (string, error) {
func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error { func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)) error {
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
for _, dm := range conflictingDMs { for _, dm := range conflictingDMs {
if isRunit() {
if !runitServiceEnabled(dm) {
continue
}
logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm))
if err := disableRunitService(dm, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err))
} else {
logFunc(fmt.Sprintf("✓ Disabled %s", dm))
}
continue
}
state, err := checkSystemdEnabled(dm) state, err := checkSystemdEnabled(dm)
if err != nil || state == "" || state == "not-found" { if err != nil || state == "" || state == "not-found" {
continue continue
@@ -2426,19 +2294,6 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string)
// EnableGreetd unmasks and enables greetd, forcing it over any other DM. // EnableGreetd unmasks and enables greetd, forcing it over any other DM.
func EnableGreetd(sudoPassword string, logFunc func(string)) error { func EnableGreetd(sudoPassword string, logFunc func(string)) error {
if isRunit() {
if !runitServiceInstalled("greetd") {
return fmt.Errorf("greetd service not found in %s; ensure greetd is installed", runitSvDir)
}
ensureRunitSeat(DetectGreeterUser(), sudoPassword, logFunc)
ensureGreetdPamRundir(sudoPassword, logFunc)
if err := enableRunitService("greetd", sudoPassword); err != nil {
return fmt.Errorf("failed to enable greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ greetd enabled (%s)", runitServiceDir))
return nil
}
state, err := checkSystemdEnabled("greetd") state, err := checkSystemdEnabled("greetd")
if err != nil { if err != nil {
return fmt.Errorf("failed to check greetd state: %w", err) return fmt.Errorf("failed to check greetd state: %w", err)
@@ -2462,11 +2317,6 @@ func EnableGreetd(sudoPassword string, logFunc func(string)) error {
} }
func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error { func EnsureGraphicalTarget(sudoPassword string, logFunc func(string)) error {
if isRunit() {
logFunc("✓ runit detected; no graphical target is needed")
return nil
}
cmd := exec.Command("systemctl", "get-default") cmd := exec.Command("systemctl", "get-default")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
+1 -6
View File
@@ -236,18 +236,13 @@ func (r *Runner) Run() error {
r.log("Starting configuration deployment") r.log("Starting configuration deployment")
deployer := config.NewConfigDeployer(r.logChan) deployer := config.NewConfigDeployer(r.logChan)
useSystemd := true results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
if distroConfig, exists := distros.Registry[osInfo.Distribution.ID]; exists && distroConfig.Family == distros.FamilyVoid {
useSystemd = false
}
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(
context.Background(), context.Background(),
wm, wm,
terminal, terminal,
dependencies, dependencies,
replaceConfigs, replaceConfigs,
reinstallItems, reinstallItems,
useSystemd,
) )
if err != nil { if err != nil {
return fmt.Errorf("configuration deployment failed: %w", err) return fmt.Errorf("configuration deployment failed: %w", err)
+3 -5
View File
@@ -68,8 +68,6 @@ func GetQtLoggingRules() string {
level = "info" level = "info"
} }
// scene carries QML engine warnings (e.g. QQuickImage "Cannot open" cache
// probes); suppressed except at debug level
var rules []string var rules []string
switch strings.ToLower(level) { switch strings.ToLower(level) {
case "fatal": case "fatal":
@@ -77,13 +75,13 @@ func GetQtLoggingRules() string {
case "error": case "error":
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"} rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
case "warn", "warning": case "warn", "warning":
rules = []string{"*.debug=false", "*.info=false", "scene.warning=false"} rules = []string{"*.debug=false", "*.info=false"}
case "info": case "info":
rules = []string{"*.debug=false", "scene.warning=false"} rules = []string{"*.debug=false"}
case "debug": case "debug":
return "" return ""
default: default:
rules = []string{"*.debug=false", "scene.warning=false"} rules = []string{"*.debug=false"}
} }
return strings.Join(rules, ";") return strings.Join(rules, ";")
+205
View File
@@ -0,0 +1,205 @@
// Package traywatcher implements a minimal org.kde.StatusNotifierWatcher that
// claims the SNI name early in the session so autostart tray apps can register
// before the shell finishes loading. See docs/TRAY_WATCHER.md.
package traywatcher
import (
"fmt"
"sort"
"strings"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
"github.com/godbus/dbus/v5/prop"
)
const (
busName = "org.kde.StatusNotifierWatcher"
objPath = dbus.ObjectPath("/StatusNotifierWatcher")
iface = "org.kde.StatusNotifierWatcher"
)
const introXML = `<node>
<interface name="org.kde.StatusNotifierWatcher">
<method name="RegisterStatusNotifierItem">
<arg type="s" direction="in" name="service"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg type="s" direction="in" name="service"/>
</method>
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<signal name="StatusNotifierItemRegistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierHostRegistered"/>
<signal name="StatusNotifierHostUnregistered"/>
</interface>
` + introspect.IntrospectDataString + prop.IntrospectDataString + `</node>`
type watcher struct {
conn *dbus.Conn
props *prop.Properties
mu sync.Mutex
items map[string]string // item id ("name/path") -> bus name to watch
hosts map[string]bool
}
func (w *watcher) itemListLocked() []string {
list := make([]string, 0, len(w.items))
for item := range w.items {
list = append(list, item)
}
sort.Strings(list)
return list
}
func (w *watcher) emit(signal string, args ...any) {
if err := w.conn.Emit(objPath, iface+"."+signal, args...); err != nil {
log.Warnf("failed to emit %s: %v", signal, err)
}
}
// Clients pass either an object path (item on the caller) or a bus name.
func (w *watcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error {
var item, watch string
if strings.HasPrefix(service, "/") {
item, watch = string(sender)+service, string(sender)
} else {
item, watch = service+"/StatusNotifierItem", service
}
w.mu.Lock()
if _, exists := w.items[item]; exists {
w.mu.Unlock()
return nil
}
w.items[item] = watch
list := w.itemListLocked()
w.mu.Unlock()
log.Infof("item registered: %s", item)
w.emit("StatusNotifierItemRegistered", item)
w.props.SetMust(iface, "RegisteredStatusNotifierItems", list)
return nil
}
func (w *watcher) RegisterStatusNotifierHost(sender dbus.Sender, service string) *dbus.Error {
watch := string(sender)
if service != "" && !strings.HasPrefix(service, "/") {
watch = service
}
w.mu.Lock()
if w.hosts[watch] {
w.mu.Unlock()
return nil
}
w.hosts[watch] = true
w.mu.Unlock()
log.Infof("host registered: %s", watch)
w.emit("StatusNotifierHostRegistered")
return nil
}
func (w *watcher) pruneOwner(name string) {
w.mu.Lock()
var removed []string
for item, watch := range w.items {
if watch == name {
delete(w.items, item)
removed = append(removed, item)
}
}
list := w.itemListLocked()
hostGone := w.hosts[name]
delete(w.hosts, name)
w.mu.Unlock()
for _, item := range removed {
log.Infof("item gone: %s", item)
w.emit("StatusNotifierItemUnregistered", item)
}
if len(removed) > 0 {
w.props.SetMust(iface, "RegisteredStatusNotifierItems", list)
}
if hostGone {
log.Infof("host gone: %s", name)
w.emit("StatusNotifierHostUnregistered")
}
}
// Run serves the watcher until the session bus connection closes.
func Run() error {
conn, err := dbus.ConnectSessionBus()
if err != nil {
return fmt.Errorf("connect to session bus: %w", err)
}
defer conn.Close()
w := &watcher{conn: conn, items: map[string]string{}, hosts: map[string]bool{}}
if err := conn.Export(w, objPath, iface); err != nil {
return fmt.Errorf("export watcher: %w", err)
}
w.props, err = prop.Export(conn, objPath, map[string]map[string]*prop.Prop{
iface: {
"RegisteredStatusNotifierItems": {Value: []string{}, Emit: prop.EmitTrue},
// Always true: libappindicator clients drop the icon if it is false.
"IsStatusNotifierHostRegistered": {Value: true, Emit: prop.EmitFalse},
"ProtocolVersion": {Value: int32(0), Emit: prop.EmitFalse},
},
})
if err != nil {
return fmt.Errorf("export properties: %w", err)
}
if err := conn.Export(introspect.Introspectable(introXML), objPath, "org.freedesktop.DBus.Introspectable"); err != nil {
return fmt.Errorf("export introspection: %w", err)
}
if err := conn.AddMatchSignal(
dbus.WithMatchSender("org.freedesktop.DBus"),
dbus.WithMatchInterface("org.freedesktop.DBus"),
dbus.WithMatchMember("NameOwnerChanged"),
dbus.WithMatchObjectPath("/org/freedesktop/DBus"),
); err != nil {
return fmt.Errorf("match NameOwnerChanged: %w", err)
}
sigs := make(chan *dbus.Signal, 128)
conn.Signal(sigs)
// No flags: queue for the name and take it when the current owner exits.
reply, err := conn.RequestName(busName, 0)
if err != nil {
return fmt.Errorf("request %s: %w", busName, err)
}
if reply == dbus.RequestNameReplyInQueue {
log.Infof("name busy, queued for %s", busName)
}
for sig := range sigs {
switch sig.Name {
case "org.freedesktop.DBus.NameAcquired":
if len(sig.Body) == 1 && sig.Body[0] == busName {
log.Infof("acquired %s", busName)
}
case "org.freedesktop.DBus.NameOwnerChanged":
if len(sig.Body) != 3 {
continue
}
name, _ := sig.Body[0].(string)
newOwner, _ := sig.Body[2].(string)
if name != busName && newOwner == "" {
w.pruneOwner(name)
}
}
}
return fmt.Errorf("session bus connection closed")
}
+1 -51
View File
@@ -9,7 +9,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -130,7 +129,7 @@ func (m Model) deployConfigurations() tea.Cmd {
deployer := config.NewConfigDeployer(m.logChan) deployer := config.NewConfigDeployer(m.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstallsAndSystemd(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems, m.useSystemdConfig()) results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(context.Background(), wm, terminal, m.dependencies, m.replaceConfigs, m.reinstallItems)
return configDeploymentResult{ return configDeploymentResult{
results: results, results: results,
@@ -139,17 +138,6 @@ func (m Model) deployConfigurations() tea.Cmd {
} }
} }
func (m Model) useSystemdConfig() bool {
if m.osInfo == nil {
return true
}
distroConfig, exists := distros.Registry[m.osInfo.Distribution.ID]
if !exists {
return true
}
return distroConfig.Family != distros.FamilyVoid
}
func (m Model) viewConfigConfirmation() string { func (m Model) viewConfigConfirmation() string {
var b strings.Builder var b strings.Builder
@@ -207,50 +195,12 @@ func (m Model) viewConfigConfirmation() string {
b.WriteString(backup) b.WriteString(backup)
b.WriteString("\n\n") b.WriteString("\n\n")
if note := m.configReplacementNote(); note != "" {
b.WriteString(m.styles.Subtle.Render(note))
b.WriteString("\n\n")
}
help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue") help := m.styles.Subtle.Render("↑/↓: Navigate, Space: Toggle replace/keep, Enter: Continue")
b.WriteString(help) b.WriteString(help)
return b.String() return b.String()
} }
func (m Model) configReplacementNote() string {
if m.selectedConfig < 0 || m.selectedConfig >= len(m.existingConfigs) {
return ""
}
configInfo := m.existingConfigs[m.selectedConfig]
if !configInfo.Exists {
return ""
}
switch configInfo.ConfigType {
case "Niri":
if m.useSystemdConfig() {
return "Replacing Niri writes the DMS Niri template and uses the user systemd dms service for shell autostart."
}
return `Replacing Niri writes the DMS Niri template and starts DMS from Niri with spawn-at-startup "dms" "run".`
case "Hyprland":
if m.useSystemdConfig() {
return "Replacing Hyprland writes the DMS Lua template and uses the user systemd dms service for shell autostart."
}
return `Replacing Hyprland writes the DMS Lua template and starts DMS from Hyprland with hl.exec_cmd("dms run").`
case "Mango":
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec-once=dms run."
case "Ghostty":
return "Replacing Ghostty writes the DMS terminal defaults and theme include."
case "Kitty":
return "Replacing Kitty writes the DMS terminal defaults, theme include, and tab styling."
case "Alacritty":
return "Replacing Alacritty writes the DMS terminal defaults and theme import."
default:
return ""
}
}
func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) updateConfigConfirmationState(msg tea.Msg) (tea.Model, tea.Cmd) {
if result, ok := msg.(configCheckResult); ok { if result, ok := msg.(configCheckResult); ok {
if result.error != nil { if result.error != nil {
+10 -33
View File
@@ -140,7 +140,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
return "dms-shell-git" return "dms-shell-git"
} }
return "dms-shell" return "dms-shell"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE, distros.FamilyVoid: case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit { if isGit {
return "dms-git" return "dms-git"
} }
@@ -168,8 +168,6 @@ func uninstallCommand(distroID string, dependencies []deps.Dependency) string {
return "sudo apt remove " + pkg return "sudo apt remove " + pkg
case distros.FamilySUSE: case distros.FamilySUSE:
return "sudo zypper remove " + pkg return "sudo zypper remove " + pkg
case distros.FamilyVoid:
return "sudo xbps-remove -R " + pkg
default: default:
return "" return ""
} }
@@ -203,26 +201,15 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager() wm := m.selectedWindowManager()
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\"" loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
if !m.useSystemdConfig() { switch wm {
switch wm { case deps.WindowManagerNiri:
case deps.WindowManagerNiri: loginHint = "If you do not have a greeter, login with \"niri-session\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session niri" case deps.WindowManagerHyprland:
case deps.WindowManagerHyprland: loginHint = "If you do not have a greeter, login with \"Hyprland\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session Hyprland" case deps.WindowManagerMango:
case deps.WindowManagerMango: loginHint = "If you do not have a greeter, login with \"mango\""
loginHint = "If you do not have a greeter, from a TTY run: dbus-run-session mango"
}
} else {
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
switch wm {
case deps.WindowManagerNiri:
loginHint = "If you do not have a greeter, login with \"niri-session\""
case deps.WindowManagerHyprland:
loginHint = "If you do not have a greeter, login with \"Hyprland\""
case deps.WindowManagerMango:
loginHint = "If you do not have a greeter, login with \"mango\""
}
} }
b.WriteString("\n") b.WriteString("\n")
@@ -235,17 +222,7 @@ func (m Model) viewInstallComplete() string {
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle)) labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Subtle))
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n") b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if !m.useSystemdConfig() { if wm == deps.WindowManagerMango {
switch wm {
case deps.WindowManagerNiri:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove spawn-at-startup "dms" "run" from ~/.config/niri/config.kdl`) + "\n")
case deps.WindowManagerHyprland:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render(`remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`) + "\n")
case deps.WindowManagerMango:
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
}
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("quickshell --path ~/.config/quickshell/dms log") + "\n")
} else if wm == deps.WindowManagerMango {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n") b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n") b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else { } else {
+1
View File
@@ -72,6 +72,7 @@ override_dh_auto_install:
if [ -d quickshell ]; then \ if [ -d quickshell ]; then \
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \ cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \ install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
install -Dm644 assets/systemd/dms-tray-watcher.service debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service; \
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \ install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \ install -Dm644 assets/com.danklinux.dms.notepad.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.notepad.desktop; \
+1
View File
@@ -65,6 +65,7 @@ override_dh_auto_install:
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \ if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \ cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms-tray-watcher.service debian/dms/usr/lib/systemd/user/dms-tray-watcher.service; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.notepad.desktop debian/dms/usr/share/applications/com.danklinux.dms.notepad.desktop; \
+2
View File
@@ -119,6 +119,7 @@ core/bin/${DMS_BINARY} completion fish > %{buildroot}%{_datadir}/fish/vendor_com
# Install systemd user service # Install systemd user service
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -145,6 +146,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%doc quickshell/README.md %doc quickshell/README.md
%{_datadir}/quickshell/dms/ %{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop
+2
View File
@@ -87,6 +87,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : %{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 %{_builddir}/dms-qml/assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 %{_builddir}/dms-qml/assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -112,6 +113,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%doc README.md CONTRIBUTING.md %doc README.md CONTRIBUTING.md
%{_datadir}/quickshell/dms/ %{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop
+18
View File
@@ -101,6 +101,24 @@ in
Install.WantedBy = [ cfg.systemd.target ]; Install.WantedBy = [ cfg.systemd.target ];
}; };
# Early SNI watcher so autostart tray apps register before the shell loads.
systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable {
Unit = {
Description = "DMS Status Notifier Watcher (early tray)";
PartOf = [ cfg.systemd.target ];
Before = [ cfg.systemd.target ];
};
Service = {
Type = "dbus";
BusName = "org.kde.StatusNotifierWatcher";
ExecStart = lib.getExe cfg.package + " tray-watcher";
Restart = "on-failure";
};
Install.WantedBy = [ cfg.systemd.target ];
};
xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) { xdg.stateFile."DankMaterialShell/session.json" = lib.mkIf (cfg.session != { }) {
source = jsonFormat.generate "session.json" cfg.session; source = jsonFormat.generate "session.json" cfg.session;
}; };
+1 -1
View File
@@ -93,7 +93,7 @@ in {
text = lib.pipe cfg'.filesToInclude [ text = lib.pipe cfg'.filesToInclude [
(map (filename: "dms/${filename}")) (map (filename: "dms/${filename}"))
withOriginalConfig withOriginalConfig
(map (filename: "include optional=true \"${filename}.kdl\"")) (map (filename: "include \"${filename}.kdl\""))
(files: files ++ fixes) (files: files ++ fixes)
(builtins.concatStringsSep "\n") (builtins.concatStringsSep "\n")
]; ];
+18
View File
@@ -39,6 +39,24 @@ in
}; };
}; };
# Early SNI watcher so autostart tray apps register before the shell loads.
systemd.user.services.dms-tray-watcher = lib.mkIf cfg.systemd.enable {
description = "DMS Status Notifier Watcher (early tray)";
path = lib.mkForce [ ];
partOf = [ cfg.systemd.target ];
before = [ cfg.systemd.target ];
wantedBy = [ cfg.systemd.target ];
restartIfChanged = cfg.systemd.restartIfChanged;
serviceConfig = {
Type = "dbus";
BusName = "org.kde.StatusNotifierWatcher";
ExecStart = lib.getExe cfg.package + " tray-watcher";
Restart = "on-failure";
};
};
environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages; environment.systemPackages = [ cfg.quickshell.package ] ++ common.packages;
environment.etc = lib.mapAttrs' (name: value: { environment.etc = lib.mapAttrs' (name: value: {
+2 -2
View File
@@ -6,8 +6,8 @@
let let
homeManagerNixosModule = homeManagerNixosModule =
(fetchTarball { (fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz"; url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b"; sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
}) })
+ "/nixos"; + "/nixos";
in in
+4 -4
View File
@@ -6,8 +6,8 @@
let let
homeManagerNixosModule = homeManagerNixosModule =
(fetchTarball { (fetchTarball {
url = "https://github.com/nix-community/home-manager/archive/53ebbdc405acc04acd9bb73ccca462b51ddb8c6d.tar.gz"; url = "https://github.com/nix-community/home-manager/archive/e82d4a4ecd18363aa2054cbaa3e32e4134c3dbf4.tar.gz";
sha256 = "1cqmfgwb3jac2zzv82bwvgypxff1z30xkz9j6qcinkmqf58j3k3b"; sha256 = "sha256-ZTYDofOM3/PJhRF1EuBh6uibm+DmkhU7Wor6mMN7YTc=";
}) })
+ "/nixos"; + "/nixos";
@@ -76,8 +76,8 @@ pkgs.testers.runNixOSTest {
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'") machine.succeed("su -- danklinux -c 'test -f ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'") machine.succeed("su -- danklinux -c 'grep -F \"include \\\"dms/binds.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"include optional=true \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'") machine.succeed("su -- danklinux -c 'grep -F \"include \\\"hm.kdl\\\"\" ~/.config/niri/config.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'") machine.succeed("su -- danklinux -c 'grep -F \"spawn-at-startup\" ~/.config/niri/hm.kdl'")
machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'") machine.succeed("su -- danklinux -c 'grep -F \"\\\"dms\\\" \\\"run\\\"\" ~/.config/niri/hm.kdl'")
''; '';
+2
View File
@@ -114,6 +114,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : ./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -152,6 +153,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%dir %{_datadir}/quickshell %dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/ %{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop
+2
View File
@@ -63,6 +63,7 @@ install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || : ./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -Dm644 assets/systemd/dms-tray-watcher.service %{buildroot}%{_userunitdir}/dms-tray-watcher.service
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop install -Dm644 assets/com.danklinux.dms.desktop %{buildroot}%{_datadir}/applications/com.danklinux.dms.desktop
@@ -99,6 +100,7 @@ pkill -USR1 -x dms >/dev/null 2>&1 || :
%dir %{_datadir}/quickshell %dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/ %{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service %{_userunitdir}/dms.service
%{_userunitdir}/dms-tray-watcher.service
%{_datadir}/applications/dms-open.desktop %{_datadir}/applications/dms-open.desktop
%{_datadir}/applications/com.danklinux.dms.desktop %{_datadir}/applications/com.danklinux.dms.desktop
%{_datadir}/applications/com.danklinux.dms.notepad.desktop %{_datadir}/applications/com.danklinux.dms.notepad.desktop
+2
View File
@@ -69,6 +69,8 @@ override_dh_auto_install:
# Install systemd user service # Install systemd user service
install -Dm644 dms-git-repo/assets/systemd/dms.service \ install -Dm644 dms-git-repo/assets/systemd/dms.service \
debian/dms-git/usr/lib/systemd/user/dms.service debian/dms-git/usr/lib/systemd/user/dms.service
install -Dm644 dms-git-repo/assets/systemd/dms-tray-watcher.service \
debian/dms-git/usr/lib/systemd/user/dms-tray-watcher.service
# Install desktop file and icon # Install desktop file and icon
install -Dm644 dms-git-repo/assets/dms-open.desktop \ install -Dm644 dms-git-repo/assets/dms-open.desktop \
+2
View File
@@ -50,6 +50,8 @@ override_dh_auto_install:
# Install systemd user service (before copying everything else) # Install systemd user service (before copying everything else)
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \ install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms.service \
debian/dms/usr/lib/systemd/user/dms.service debian/dms/usr/lib/systemd/user/dms.service
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/systemd/dms-tray-watcher.service \
debian/dms/usr/lib/systemd/user/dms-tray-watcher.service
# Install desktop file and icon # Install desktop file and icon
install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \ install -Dm644 DankMaterialShell-$(BASE_VERSION)/assets/dms-open.desktop \
+2 -23
View File
@@ -52,8 +52,8 @@ checkout at `srcpkgs/<pkg>/template` to build or submit it.
## Dependencies ## Dependencies
Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`, Installing `dms` automatically pulls in `quickshell`, `accountsservice`, `dgop`,
`matugen` (which drives the Material You theming), `dbus`, and `elogind`. and `matugen` (which drives the Material You theming). The rest are optional —
The rest are optional, install whichever features you want: install whichever features you want:
| Package | Enables | | Package | Enables |
| --- | --- | | --- | --- |
@@ -98,27 +98,6 @@ spawn-at-startup "dms" "run"
or Hyprland: `exec-once = dms run`. or Hyprland: `exec-once = dms run`.
From a TTY on Void without a greeter, start your compositor through a D-Bus
session:
```sh
dbus-run-session niri
dbus-run-session Hyprland
dbus-run-session mango
```
The `mangowc` package provides the `mango` command.
For power menu actions to work on runit systems, make sure the system D-Bus and
elogind services are enabled:
```sh
sudo ln -sf /etc/sv/dbus /var/service/dbus
sudo ln -sf /etc/sv/elogind /var/service/elogind
```
The `dankinstall` Void path does this automatically after installing packages.
## Greeter (optional) ## Greeter (optional)
Install `dms-greeter`, then let the CLI do the setup: Install `dms-greeter`, then let the CLI do the setup:
+2 -1
View File
@@ -32,12 +32,13 @@ conflicts="dms"
provides="dms-${version}_${revision}" provides="dms-${version}_${revision}"
# Optional feature deps are listed in distro/void/README.md. # Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus elogind" depends="quickshell accountsservice dgop matugen dbus"
post_install() { post_install() {
# QML shell tree (build_style=go already installed the dms binary) # QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon # Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
+2 -1
View File
@@ -22,12 +22,13 @@ distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v$
checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61 checksum=f54601e522c883fa9cce02bec070e4321e47389a1cf453e7ad0bb7379ad91b61
# Optional feature deps are listed in distro/void/README.md. # Optional feature deps are listed in distro/void/README.md.
depends="quickshell accountsservice dgop matugen dbus elogind" depends="quickshell accountsservice dgop matugen dbus"
post_install() { post_install() {
# QML shell tree (build_style=go already installed the dms binary) # QML shell tree (build_style=go already installed the dms binary)
vmkdir usr/share/quickshell/dms vmkdir usr/share/quickshell/dms
vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms vcopy "${wrksrc}/quickshell/*" usr/share/quickshell/dms
echo "${version}" > "${DESTDIR}/usr/share/quickshell/dms/VERSION"
# Desktop entry + icon # Desktop entry + icon
vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications vinstall "${wrksrc}/assets/dms-open.desktop" 644 usr/share/applications
Generated
+3 -3
View File
@@ -18,11 +18,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1783224372, "lastModified": 1778443072,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d407951447dcd00442e97087bf374aad70c04cea", "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "github" "type": "github"
}, },
"original": { "original": {
+2 -17
View File
@@ -120,18 +120,12 @@ Singleton {
function setMonitorScrollPosition(screenName, scrollX, scrollY) { function setMonitorScrollPosition(screenName, scrollX, scrollY) {
var newPositions = Object.assign({}, monitorScrollPositions); var newPositions = Object.assign({}, monitorScrollPositions);
newPositions[screenName] = { newPositions[screenName] = { scrollX: scrollX, scrollY: scrollY };
scrollX: scrollX,
scrollY: scrollY
};
monitorScrollPositions = newPositions; monitorScrollPositions = newPositions;
} }
function getMonitorScrollPosition(screenName) { function getMonitorScrollPosition(screenName) {
return monitorScrollPositions[screenName] || { return monitorScrollPositions[screenName] || { scrollX: 50, scrollY: 50 };
scrollX: 50,
scrollY: 50
};
} }
function clearMonitorScrollPosition(screenName) { function clearMonitorScrollPosition(screenName) {
@@ -215,8 +209,6 @@ Singleton {
property string locale: "" property string locale: ""
property string timeLocale: "" property string timeLocale: ""
property string notepadLastMode: ""
property string launcherLastMode: "all" property string launcherLastMode: "all"
property string launcherLastFileSearchType: "all" property string launcherLastFileSearchType: "all"
property string launcherLastQuery: "" property string launcherLastQuery: ""
@@ -1224,13 +1216,6 @@ Singleton {
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json"); I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
} }
function setNotepadLastMode(mode) {
if (notepadLastMode === mode)
return;
notepadLastMode = mode;
saveSettings();
}
function setLauncherLastMode(mode) { function setLauncherLastMode(mode) {
launcherLastMode = mode; launcherLastMode = mode;
saveSettings(); saveSettings();
@@ -88,8 +88,6 @@ var SPEC = {
locale: { def: "", onChange: "updateLocale" }, locale: { def: "", onChange: "updateLocale" },
timeLocale: { def: "" }, timeLocale: { def: "" },
notepadLastMode: { def: "" },
launcherLastMode: { def: "all" }, launcherLastMode: { def: "all" },
launcherLastFileSearchType: { def: "all" }, launcherLastFileSearchType: { def: "all" },
launcherLastQuery: { def: "" }, launcherLastQuery: { def: "" },
+5 -6
View File
@@ -373,7 +373,7 @@ Item {
} }
function open(): string { function open(): string {
if (PopoutService.notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.openNotepadPopout(); PopoutService.openNotepadPopout();
return "NOTEPAD_OPEN_SUCCESS"; return "NOTEPAD_OPEN_SUCCESS";
} }
@@ -388,7 +388,7 @@ Item {
function openFile(path: string): string { function openFile(path: string): string {
if (!path) if (!path)
return open(); return open();
if (PopoutService.notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.openNotepadPopoutWithFile(path); PopoutService.openNotepadPopoutWithFile(path);
return "NOTEPAD_OPEN_FILE_SUCCESS"; return "NOTEPAD_OPEN_FILE_SUCCESS";
} }
@@ -402,7 +402,7 @@ Item {
} }
function close(): string { function close(): string {
if (PopoutService.notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.notepadPopout?.hide(); PopoutService.notepadPopout?.hide();
return "NOTEPAD_CLOSE_SUCCESS"; return "NOTEPAD_CLOSE_SUCCESS";
} }
@@ -415,7 +415,7 @@ Item {
} }
function toggle(): string { function toggle(): string {
if (PopoutService.notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.toggleNotepadPopout(); PopoutService.toggleNotepadPopout();
return "NOTEPAD_TOGGLE_SUCCESS"; return "NOTEPAD_TOGGLE_SUCCESS";
} }
@@ -712,13 +712,12 @@ Item {
target: "hypr" target: "hypr"
} }
// ! TODO - remove for v1.6
IpcHandler { IpcHandler {
function wallpaper(): string { function wallpaper(): string {
const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar(); const bar = root.getPreferredBar("clockButtonRef") || root.getPreferredBar();
if (bar) { if (bar) {
bar.triggerWallpaperBrowser(); bar.triggerWallpaperBrowser();
return "WARN; deprecated, use dms ipc call dash toggle wallpaper instead"; return "SUCCESS: Toggled wallpaper browser";
} }
return "ERROR: Failed to toggle wallpaper browser"; return "ERROR: Failed to toggle wallpaper browser";
} }
@@ -65,7 +65,7 @@ Column {
StyledText { StyledText {
id: codenameText id: codenameText
anchors.centerIn: parent anchors.centerIn: parent
text: "The Wolverine" text: "Saffron Bloom"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.primary color: Theme.primary
@@ -74,7 +74,7 @@ Column {
} }
StyledText { StyledText {
text: "Frame Mode, DankCalendar, Spotlight, & more" text: "New launcher, enhanced plugin system, KDE Connect, & more"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -108,99 +108,77 @@ Column {
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "border_outer" iconName: "space_dashboard"
title: "Frame Mode" title: "Dank Launcher V2"
description: "Connected shell surfaces" description: "New capabilities & plugins"
onClicked: PopoutService.openSettingsWithTab("frame") onClicked: PopoutService.openDankLauncherV2()
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "calendar_month" iconName: "smartphone"
title: "DankCalendar" title: "Phone Connect"
description: "Native calendar & events" description: "KDE Connect & Valent"
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dankcalendar") onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "search" iconName: "monitor_heart"
title: "Spotlight" title: "System Monitor"
description: "Lightweight launcher" description: "Redesigned process list"
onClicked: PopoutService.openSpotlightBar() onClicked: PopoutService.showProcessListModal()
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "window" iconName: "window"
title: "Window Rules" title: "Window Rules"
description: "Rules for all compositors" description: "niri window rule manager"
visible: CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("window_rules") onClicked: PopoutService.openSettingsWithTab("window_rules")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "display_settings" iconName: "notifications_active"
title: "Display Profiles" title: "Enhanced Notifications"
description: "Auto-switch monitor layouts" description: "Configurable rules & styling"
onClicked: PopoutService.openSettingsWithTab("display_config") visible: !CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("notifications")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "dvr" iconName: "dock_to_bottom"
title: "Multiplexer Launcher" title: "Dock Enhancements"
description: "Attach to tmux sessions" description: "Bar dock widget & more"
onClicked: PopoutService.openSettingsWithTab("multiplexers") onClicked: PopoutService.openSettingsWithTab("dock")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "edit_note" iconName: "volume_up"
title: "Notepad Rewrite" title: "Audio Aliases"
description: "Popout & tiling support" description: "Custom device names"
onClicked: PopoutService.openNotepad() onClicked: PopoutService.openSettingsWithTab("audio")
} }
ChangelogFeatureCard { ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2 width: (parent.width - Theme.spacingS) / 2
iconName: "gradient" iconName: "extension"
title: "M3 Shadows" title: "Enhanced Plugin System"
description: "Reworked elevation system" description: "Enables new types of plugins"
onClicked: PopoutService.openSettingsWithTab("plugins")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "light_mode"
title: "Auto Light/Dark"
description: "Automatic mode switching"
onClicked: PopoutService.openSettingsWithTab("theme") onClicked: PopoutService.openSettingsWithTab("theme")
} }
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "login"
title: "Greeter Enhancements"
description: "Settings GUI & multi-user"
onClicked: PopoutService.openSettingsWithTab("greeter")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "content_paste"
title: "Clipboard Filtering"
description: "Text, image & pinned filters"
onClicked: PopoutService.openSettingsWithTab("clipboard")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "restart_alt"
title: "XDG Autostart"
description: "Manage apps at login"
onClicked: PopoutService.openSettingsWithTab("autostart")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "apps"
title: "Default Apps"
description: "Set preferred applications"
onClicked: PopoutService.openSettingsWithTab("default_apps")
}
} }
} }
@@ -252,7 +230,12 @@ Column {
ChangelogUpgradeNote { ChangelogUpgradeNote {
width: parent.width width: parent.width
text: "App ID changed to com.danklinux.dms — update any compositor window rules targeting the old ID" text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
}
ChangelogUpgradeNote {
width: parent.width
text: "Plugin API updated — third-party plugins may need updates"
} }
} }
} }
@@ -131,7 +131,7 @@ FloatingWindow {
iconName: "open_in_new" iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText textColor: Theme.surfaceText
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-5-release") onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
} }
DankButton { DankButton {
@@ -1,10 +1,10 @@
import QtQuick import QtQuick
import Quickshell.Widgets import QtQuick.Effects
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ClippingRectangle { Item {
id: thumbnail id: thumbnail
readonly property var log: Log.scoped("ClipboardThumbnail") readonly property var log: Log.scoped("ClipboardThumbnail")
@@ -15,10 +15,6 @@ ClippingRectangle {
required property int itemIndex required property int itemIndex
property bool disposed: false property bool disposed: false
radius: Theme.cornerRadius / 2
color: "transparent"
antialiasing: true
Image { Image {
id: thumbnailImage id: thumbnailImage
@@ -38,7 +34,7 @@ ClippingRectangle {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
cache: false cache: false
visible: entryType === "image" && status === Image.Ready && source != "" visible: false
asynchronous: true asynchronous: true
sourceSize.width: 128 sourceSize.width: 128
sourceSize.height: 128 sourceSize.height: 128
@@ -240,6 +236,33 @@ ClippingRectangle {
} }
} }
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: thumbnailImage
maskEnabled: true
maskSource: clipboardRoundedRectangularMask
visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: clipboardRoundedRectangularMask
width: ClipboardConstants.thumbnailSize
height: ClipboardConstants.itemHeight - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
color: "black"
antialiasing: true
}
}
DankIcon { DankIcon {
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "") visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
name: { name: {
@@ -62,11 +62,6 @@ Item {
property bool animationsEnabled: true property bool animationsEnabled: true
function _kickBlurCommit() {
if (typeof contentWindow.update === "function")
contentWindow.update();
}
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
isClosing = false; isClosing = false;
@@ -206,12 +201,6 @@ Item {
} }
})(), dpr) })(), dpr)
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
onAlignedHeightChanged: _kickBlurCommit()
onShouldBeVisibleChanged: _kickBlurCommit()
PanelWindow { PanelWindow {
id: clickCatcher id: clickCatcher
visible: false visible: false
@@ -255,13 +244,11 @@ Item {
WindowBlur { WindowBlur {
targetWindow: contentWindow targetWindow: contentWindow
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.06) * 2))
readonly property real visibleScale: s * op
// Blur tracks the surface's scaled rect, matching the connected backend. // Blur tracks the surface's scaled rect, matching the connected backend.
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: root.shouldBeVisible ? modalContainer.width * visibleScale : 0 blurWidth: root.shouldBeVisible ? modalContainer.width * s : 0
blurHeight: root.shouldBeVisible ? modalContainer.height * visibleScale : 0 blurHeight: root.shouldBeVisible ? modalContainer.height * s : 0
blurRadius: root.cornerRadius blurRadius: root.cornerRadius
} }
@@ -351,7 +338,6 @@ Item {
QtObject { QtObject {
id: morph id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0 property real openProgress: root.shouldBeVisible ? 1 : 0
onOpenProgressChanged: root._kickBlurCommit()
Behavior on openProgress { Behavior on openProgress {
enabled: root.animationsEnabled enabled: root.animationsEnabled
DankAnim { DankAnim {
@@ -13,7 +13,7 @@ Rectangle {
property string cachedMimeType: "" property string cachedMimeType: ""
property var _requestedEntryId: null property var _requestedEntryId: null
readonly property bool canLoadImage: typeof entry?.id === "number" && !!entry?.isImage && String(entry?.mimeType ?? "").startsWith("image/") readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? "")) readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
radius: Math.max(6, Theme.cornerRadius - 2) radius: Math.max(6, Theme.cornerRadius - 2)
@@ -41,18 +41,13 @@ Rectangle {
} }
function reloadPreview() { function reloadPreview() {
if (!canLoadImage || typeof entry?.id !== "number") {
_requestedEntryId = null;
cachedImageData = "";
cachedMimeType = "";
return;
}
// Entry objects are rebuilt per search; same id means same content
if (entry.id === _requestedEntryId)
return;
cachedImageData = ""; cachedImageData = "";
cachedMimeType = ""; cachedMimeType = "";
if (!canLoadImage || !entry?.id) {
_requestedEntryId = null;
return;
}
const entryId = entry.id; const entryId = entry.id;
_requestedEntryId = entryId; _requestedEntryId = entryId;
DMSService.sendRequest("clipboard.getEntry", { DMSService.sendRequest("clipboard.getEntry", {
@@ -60,22 +55,17 @@ Rectangle {
}, function (response) { }, function (response) {
if (_requestedEntryId !== entryId) if (_requestedEntryId !== entryId)
return; return;
if (response.error) { if (response.error)
_requestedEntryId = null;
return; return;
}
if (!response.result) { if (!response.result) {
_requestedEntryId = null;
ClipboardService.refresh(); ClipboardService.refresh();
return; return;
} }
const result = response.result; const result = response.result;
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString(); const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
const data = (result.data ?? "").toString(); const data = (result.data ?? "").toString();
if (data.length === 0 || !resolvedSourceUrl(data, mimeType)) { if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
_requestedEntryId = null;
return; return;
}
cachedMimeType = mimeType; cachedMimeType = mimeType;
cachedImageData = data; cachedImageData = data;
}); });
@@ -88,8 +78,6 @@ Rectangle {
asynchronous: true asynchronous: true
cache: false cache: false
smooth: true smooth: true
sourceSize.width: 128
sourceSize.height: 128
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
visible: status === Image.Ready visible: status === Image.Ready
} }
+14 -57
View File
@@ -50,15 +50,15 @@ Item {
} }
onActiveChanged: { onActiveChanged: {
ClipboardService.invalidateLauncherSearchCache(); if (!active) {
if (active) SessionData.addLauncherHistory(searchQuery);
return;
SessionData.addLauncherHistory(searchQuery); sections = [];
sections = []; flatModel = [];
flatModel = []; selectedItem = null;
selectedItem = null; _clearModeCache();
_clearModeCache(); ClipboardService.invalidateLauncherSearchCache();
}
} }
onSearchModeChanged: { onSearchModeChanged: {
@@ -110,7 +110,7 @@ Item {
if (query !== effectiveQuery) if (query !== effectiveQuery)
return; return;
root.requestSearch(); searchDebounce.restart();
} }
} }
@@ -276,23 +276,9 @@ Item {
property string appCategory: "" property string appCategory: ""
property var appCategories: [] property var appCategories: []
function builtInSectionViewPref(sectionId) {
switch (sectionId) {
case "clipboard":
return getPluginViewPref("dms_clipboard_search");
case "settings":
return getPluginViewPref("dms_settings_search");
default:
return null;
}
}
function getSectionViewMode(sectionId) { function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins") if (sectionId === "browse_plugins")
return "list"; return "list";
var builtInPref = builtInSectionViewPref(sectionId);
if (builtInPref?.enforced)
return builtInPref.mode;
if (pluginViewPreferences[sectionId]?.enforced) if (pluginViewPreferences[sectionId]?.enforced)
return pluginViewPreferences[sectionId].mode; return pluginViewPreferences[sectionId].mode;
if (sectionViewModes[sectionId]) if (sectionViewModes[sectionId])
@@ -316,8 +302,6 @@ Item {
function setSectionViewMode(sectionId, mode) { function setSectionViewMode(sectionId, mode) {
if (sectionId === "browse_plugins") if (sectionId === "browse_plugins")
return; return;
if (builtInSectionViewPref(sectionId)?.enforced)
return;
if (pluginViewPreferences[sectionId]?.enforced) if (pluginViewPreferences[sectionId]?.enforced)
return; return;
sectionViewModes = Object.assign({}, sectionViewModes, { sectionViewModes = Object.assign({}, sectionViewModes, {
@@ -341,8 +325,6 @@ Item {
function canChangeSectionViewMode(sectionId) { function canChangeSectionViewMode(sectionId) {
if (sectionId === "browse_plugins") if (sectionId === "browse_plugins")
return false; return false;
if (builtInSectionViewPref(sectionId)?.enforced)
return false;
return !pluginViewPreferences[sectionId]?.enforced; return !pluginViewPreferences[sectionId]?.enforced;
} }
@@ -398,29 +380,10 @@ Item {
property bool _pluginPhaseForceFirst: false property bool _pluginPhaseForceFirst: false
property var _phase1Items: [] property var _phase1Items: []
property bool _searchPending: false
// Leading-edge debounce: search immediately when idle, coalesce bursts
function requestSearch() {
if (searchDebounce.running) {
_searchPending = true;
searchDebounce.restart();
return;
}
_searchPending = false;
performSearch();
searchDebounce.restart();
}
Timer { Timer {
id: searchDebounce id: searchDebounce
interval: 60 interval: 60
onTriggered: { onTriggered: root.performSearch()
if (!root._searchPending)
return;
root._searchPending = false;
root.performSearch();
}
} }
Timer { Timer {
@@ -446,7 +409,7 @@ Item {
_phase1Items = []; _phase1Items = [];
pluginPhaseTimer.stop(); pluginPhaseTimer.stop();
searchQuery = query; searchQuery = query;
requestSearch(); searchDebounce.restart();
if (searchMode !== "plugins" && query.startsWith("/")) { if (searchMode !== "plugins" && query.startsWith("/")) {
var prefix = Utils.parseFileSearchPrefix(query); var prefix = Utils.parseFileSearchPrefix(query);
@@ -731,7 +694,6 @@ Item {
if (triggerMatch.isBuiltIn) { if (triggerMatch.isBuiltIn) {
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query); var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
for (var j = 0; j < builtInItems.length; j++) { for (var j = 0; j < builtInItems.length; j++) {
builtInItems[j]._preScored = 1000 - j;
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId)); allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
} }
} }
@@ -1236,12 +1198,8 @@ Item {
} }
function transformBuiltInSearchItem(item, pluginId) { function transformBuiltInSearchItem(item, pluginId) {
if (pluginId === "dms_clipboard_search" || item.type === "clipboard") { if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
var transformed = transformClipboardEntry(item.data || item); return transformClipboardEntry(item.data || item);
if (item._preScored !== undefined)
transformed._preScored = item._preScored;
return transformed;
}
return transformBuiltInLauncherItem(item, pluginId); return transformBuiltInLauncherItem(item, pluginId);
} }
@@ -1913,8 +1871,7 @@ Item {
} }
function executeSelected() { function executeSelected() {
if (_searchPending) { if (searchDebounce.running) {
_searchPending = false;
searchDebounce.stop(); searchDebounce.stop();
performSearch(); performSearch();
} }
@@ -120,18 +120,6 @@ Item {
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
// Blur region isn't auto-committed on geometry changes; kick twice to catch resize settling.
function _kickBlurCommit() {
launcherBlur.kick();
Qt.callLater(launcherBlur.kick);
}
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
on_ContentImplicitHChanged: _kickBlurCommit()
onContentVisibleChanged: _kickBlurCommit()
signal dialogClosed signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) { function _ensureContentLoadedAndInitialize(query, mode) {
@@ -340,7 +328,6 @@ Item {
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WindowBlur { WindowBlur {
id: launcherBlur
targetWindow: launcherWindow targetWindow: launcherWindow
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2)) readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
blurX: modalContainer.x blurX: modalContainer.x
@@ -350,9 +337,6 @@ Item {
blurRadius: root.cornerRadius blurRadius: root.cornerRadius
} }
onWidthChanged: root._kickBlurCommit()
onHeightChanged: root._kickBlurCommit()
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
@@ -437,9 +421,6 @@ Item {
opacity: contentVisible ? 1 : 0 opacity: contentVisible ? 1 : 0
onOpacityChanged: root._kickBlurCommit()
onSlideOffsetChanged: root._kickBlurCommit()
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: contentVisible ? root._openDuration : root._closeDuration duration: contentVisible ? root._openDuration : root._closeDuration
@@ -82,12 +82,6 @@ Item {
readonly property real windowWidth: alignedWidth + contentX + shadowPad readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: alignedHeight + contentY + shadowPad readonly property real windowHeight: alignedHeight + contentY + shadowPad
onAlignedXChanged: _kickBlurCommit()
onAlignedYChanged: _kickBlurCommit()
onAlignedWidthChanged: _kickBlurCommit()
onAlignedHeightChanged: _kickBlurCommit()
onContentVisibleChanged: _kickBlurCommit()
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground readonly property bool useBackgroundDarken: !FrameTransitionState.effectiveFrameEnabled && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
@@ -119,11 +113,6 @@ Item {
signal dialogClosed signal dialogClosed
function _kickBlurCommit() {
if (typeof launcherWindow.update === "function")
launcherWindow.update();
}
function _ensureContentLoadedAndInitialize(query, mode) { function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || ""; _pendingQuery = query || "";
_pendingMode = mode || ""; _pendingMode = mode || "";
@@ -377,13 +366,11 @@ Item {
WindowBlur { WindowBlur {
targetWindow: launcherWindow targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale) readonly property real s: Math.min(1, modalContainer.publishedScale)
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
readonly property real visibleScale: s * op
// Blur tracks the surface's scaled rect // Blur tracks the surface's scaled rect
blurX: modalContainer.x + modalContainer.width * (1 - visibleScale) * 0.5 blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - visibleScale) * 0.5 blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5
blurWidth: contentVisible ? modalContainer.width * visibleScale : 0 blurWidth: contentVisible ? modalContainer.width * s : 0
blurHeight: contentVisible ? modalContainer.height * visibleScale : 0 blurHeight: contentVisible ? modalContainer.height * s : 0
blurRadius: root.cornerRadius blurRadius: root.cornerRadius
} }
@@ -473,8 +460,6 @@ Item {
opacity: contentVisible ? 1 : 0 opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96 scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center transformOrigin: Item.Center
onOpacityChanged: root._kickBlurCommit()
onPublishedScaleChanged: root._kickBlurCommit()
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -33,7 +33,7 @@ Rectangle {
return item.icon || ""; return item.icon || "";
} }
} }
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/") readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
width: parent?.width ?? 200 width: parent?.width ?? 200
height: 52 height: 52
@@ -48,8 +48,9 @@ Rectangle {
return "file://" + raw; return "file://" + raw;
return raw; return raw;
} }
readonly property bool hasClipboardPreview: item?.type === "clipboard" && !!item?.data?.isImage && String(item?.data?.mimeType ?? "").startsWith("image/") readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
readonly property string typeLabel: { readonly property string typeLabel: {
if (!item) if (!item)
@@ -283,10 +284,16 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
source: root.previewSource source: root.previewSource
asynchronous: true asynchronous: true
sourceSize.width: 128
sourceSize.height: 128
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
visible: !root.hasClipboardPreview visible: !root.hasClipboardPreview && !root.previewAnimated
}
AnimatedImage {
anchors.fill: parent
source: root.previewSource
fillMode: Image.PreserveAspectCrop
playing: visible
visible: !root.hasClipboardPreview && root.previewAnimated
} }
ClipboardLauncherPreview { ClipboardLauncherPreview {
@@ -165,9 +165,6 @@ Rectangle {
anchors.margins: root.hasScreencopy ? 4 : 0 anchors.margins: root.hasScreencopy ? 4 : 0
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: root.item?.data?.attribution || "" source: root.item?.data?.attribution || ""
asynchronous: true
sourceSize.width: 80
sourceSize.height: 80
mipmap: true mipmap: true
} }
} }
@@ -1,5 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Widgets import QtQuick.Effects
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -98,25 +98,25 @@ StyledRect {
} }
property string _videoThumb: "" property string _videoThumb: ""
property bool _thumbGenAttempted: false
// Probe the thumbnail optimistically; Image.Error triggers generation
onVideoThumbnailPathChanged: { onVideoThumbnailPathChanged: {
_thumbGenAttempted = false;
_videoThumb = videoThumbnailPath;
}
function generateVideoThumbnail() {
if (_thumbGenAttempted)
return;
_thumbGenAttempted = true;
_videoThumb = ""; _videoThumb = "";
if (!videoThumbnailPath)
return;
const thumbPath = videoThumbnailPath; const thumbPath = videoThumbnailPath;
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize; const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f"; const size = _thumbnailPx;
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) { const fp = delegateRoot.filePath;
if (exitCode === 0) Paths.mkdir(thumbDir);
Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
if (exitCode === 0) {
_videoThumb = thumbPath; _videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
}); });
} }
@@ -160,52 +160,75 @@ StyledRect {
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8) height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
ClippingRectangle { Image {
id: gridPreviewImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 anchors.margins: 2
radius: Theme.cornerRadius property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
color: "transparent" property int weExtIndex: 0
property string imagePath: {
Image { if (weMode && delegateRoot.fileIsDir)
id: gridPreviewImage return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
anchors.fill: parent if (_videoThumb)
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"] return _videoThumb;
property int weExtIndex: 0 return "";
property string imagePath: {
if (weMode && delegateRoot.fileIsDir)
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
if (_videoThumb)
return _videoThumb;
return "";
}
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: {
if (status !== Image.Error)
return;
if (weMode && delegateRoot.fileIsDir) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++;
} else {
imagePath = "";
}
return;
}
if (_videoThumb)
generateVideoThumbnail();
}
fillMode: Image.PreserveAspectCrop
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
asynchronous: true
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
} }
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: {
if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
if (weExtIndex < weExtensions.length - 1) {
weExtIndex++;
} else {
imagePath = "";
}
}
}
fillMode: Image.PreserveAspectCrop
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
asynchronous: true
visible: false
}
CachingImage { CachingImage {
anchors.fill: parent
anchors.margins: 2
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
maxCacheSize: 256
visible: !delegateRoot.fileIsDir && isImage
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridImageMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: gridPreviewImage
maskEnabled: true
maskSource: gridImageMask
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: gridImageMask
anchors.fill: parent
anchors.margins: 2
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent anchors.fill: parent
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : "" radius: Theme.cornerRadius
maxCacheSize: 256 color: "black"
animate: false antialiasing: true
visible: !delegateRoot.fileIsDir && isImage
} }
} }
@@ -1,5 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Widgets import QtQuick.Effects
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -95,25 +95,23 @@ StyledRect {
} }
property string _videoThumb: "" property string _videoThumb: ""
property bool _thumbGenAttempted: false
// Probe the thumbnail optimistically; Image.Error triggers generation
onVideoThumbnailPathChanged: { onVideoThumbnailPathChanged: {
_thumbGenAttempted = false;
_videoThumb = videoThumbnailPath;
}
function generateVideoThumbnail() {
if (_thumbGenAttempted)
return;
_thumbGenAttempted = true;
_videoThumb = ""; _videoThumb = "";
if (!videoThumbnailPath)
return;
const thumbPath = videoThumbnailPath; const thumbPath = videoThumbnailPath;
const thumbDir = _xdgCacheHome + "/thumbnails/normal"; const fp = listDelegateRoot.filePath;
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f"; Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) { Proc.runCommand(null, ["test", "-f", thumbPath], function (output, exitCode) {
if (exitCode === 0) if (exitCode === 0) {
_videoThumb = thumbPath; _videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function (output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
}); });
} }
@@ -167,33 +165,54 @@ StyledRect {
height: 28 height: 28
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
ClippingRectangle { Image {
id: listPreviewImage
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius property string imagePath: _videoThumb
color: "transparent" source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
fillMode: Image.PreserveAspectCrop
sourceSize.width: 32
sourceSize.height: 32
asynchronous: true
visible: false
}
Image { CachingImage {
id: listPreviewImage anchors.fill: parent
anchors.fill: parent imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
property string imagePath: _videoThumb maxCacheSize: 256
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : "" visible: !listDelegateRoot.fileIsDir && isImage
fillMode: Image.PreserveAspectCrop layer.enabled: true
sourceSize.width: 32 layer.effect: MultiEffect {
sourceSize.height: 32 maskEnabled: true
asynchronous: true maskSource: listImageMask
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo maskThresholdMin: 0.5
onStatusChanged: { maskSpreadAtMin: 1
if (status === Image.Error && _videoThumb)
generateVideoThumbnail();
}
} }
}
CachingImage { MultiEffect {
anchors.fill: parent
source: listPreviewImage
maskEnabled: true
maskSource: listImageMask
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: listImageMask
anchors.fill: parent
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent anchors.fill: parent
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : "" radius: Theme.cornerRadius
maxCacheSize: 256 color: "black"
animate: false antialiasing: true
visible: !listDelegateRoot.fileIsDir && isImage
} }
} }
+31 -5
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -591,11 +591,24 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
ClippingRectangle { Rectangle {
id: gridProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: "transparent" visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: gridButtonRect.isHolding visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -716,11 +729,24 @@ DankModal {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
ClippingRectangle { Rectangle {
id: listProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: "transparent" visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: listButtonRect.isHolding visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -108,48 +108,16 @@ Variants {
function invalidate() { function invalidate() {
_settleFrames = 3; _settleFrames = 3;
backingWindow?.update(); backingWindow?.update();
if (!_wedgeBounced)
wedgeWatchdog.restart();
} }
onRenderActiveChanged: invalidate() onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate() onBackingWindowChanged: invalidate()
// Same wedge recovery as WallpaperBackground
property bool _wedgeBounced: false
Timer {
id: wedgeWatchdog
interval: 3000
repeat: false
onTriggered: {
if (!root.backingWindow || !blurWallpaperWindow.visible || IdleService.isShellLocked)
return;
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
root._wedgeBounced = true;
surfaceReattach.restart();
}
}
Timer {
id: surfaceReattach
interval: 0
repeat: false
onTriggered: {
blurWallpaperWindow.visible = false;
Qt.callLater(() => {
blurWallpaperWindow.visible = true;
});
}
}
Connections { Connections {
target: root.backingWindow target: root.backingWindow
function onFrameSwapped() { function onFrameSwapped() {
if (root._settleFrames > 0) if (root._settleFrames > 0)
root._settleFrames--; root._settleFrames--;
root._wedgeBounced = false;
wedgeWatchdog.stop();
} }
function onVisibleChanged() { function onVisibleChanged() {
root.invalidate(); root.invalidate();
@@ -174,29 +142,10 @@ Variants {
function onWallpaperFillModeChanged() { function onWallpaperFillModeChanged() {
root.invalidate(); root.invalidate();
} }
function onEffectiveWallpaperBackgroundColorChanged() { function onWallpaperBackgroundColorModeChanged() {
root.invalidate(); root.invalidate();
} }
} function onWallpaperBackgroundCustomColorChanged() {
Connections {
target: SessionData
function onMonitorWallpaperFillModesChanged() {
root.invalidate();
}
function onPerMonitorWallpaperChanged() {
root.invalidate();
}
}
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
Connections {
target: Theme
enabled: root.isColorSource || currentWallpaper.status === Image.Error
function onPrimaryChanged() {
root.invalidate();
}
function onBackgroundChanged() {
root.invalidate(); root.invalidate();
} }
} }
@@ -221,7 +170,6 @@ Variants {
} }
onSourceChanged: { onSourceChanged: {
invalidate();
if (!source || source.startsWith("#")) { if (!source || source.startsWith("#")) {
setWallpaperImmediate(""); setWallpaperImmediate("");
return; return;
@@ -242,7 +190,6 @@ Variants {
} }
function setWallpaperImmediate(newSource) { function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
@@ -7,24 +7,15 @@ Item {
id: root id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool isPlaying: activePlayer !== null && activePlayer.playbackState === MprisPlaybackState.Playing readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool live: visible && isPlaying readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
width: 20 width: 20
height: Theme.iconSize height: Theme.iconSize
readonly property real maxBarHeight: Theme.iconSize - 2
readonly property real minBarHeight: 3
onLiveChanged: {
if (!live) {
bars.bandsA = Qt.vector4d(0, 0, 0, 0);
bars.bandsB = Qt.vector2d(0, 0);
}
}
Loader { Loader {
active: root.live active: isPlaying
sourceComponent: Component { sourceComponent: Component {
Ref { Ref {
service: CavaService service: CavaService
@@ -32,8 +23,15 @@ Item {
} }
} }
readonly property real maxBarHeight: Theme.iconSize - 2
readonly property real minBarHeight: 3
readonly property real heightRange: maxBarHeight - minBarHeight
property var barHeights: [minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight, minBarHeight]
Timer { Timer {
running: !CavaService.cavaAvailable && root.live id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
interval: 500 interval: 500
repeat: true repeat: true
onTriggered: { onTriggered: {
@@ -43,32 +41,54 @@ Item {
Connections { Connections {
target: CavaService target: CavaService
enabled: root.live
function onValuesChanged() { function onValuesChanged() {
const v = CavaService.values; if (!root.isPlaying) {
if (v.length < 6) root.barHeights = [root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight, root.minBarHeight];
return; return;
const n = i => { }
const x = v[i];
return x <= 0 ? 0 : x >= 100 ? 1 : Math.sqrt(x * 0.01); const newHeights = [];
}; for (let i = 0; i < 6; i++) {
bars.bandsA = Qt.vector4d(n(0), n(1), n(2), n(3)); if (CavaService.values.length <= i) {
bars.bandsB = Qt.vector2d(n(4), n(5)); newHeights.push(root.minBarHeight);
continue;
}
const rawLevel = CavaService.values[i];
if (rawLevel <= 0) {
newHeights.push(root.minBarHeight);
} else if (rawLevel >= 100) {
newHeights.push(root.maxBarHeight);
} else {
newHeights.push(root.minBarHeight + Math.sqrt(rawLevel * 0.01) * root.heightRange);
}
}
root.barHeights = newHeights;
} }
} }
ShaderEffect { Row {
id: bars anchors.centerIn: parent
anchors.fill: parent spacing: 1.5
property real widthPx: width Repeater {
property real heightPx: height model: 6
property real minH: root.minBarHeight
property real maxH: root.maxBarHeight
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0)
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
fragmentShader: Qt.resolvedUrl("../../../Shaders/qsb/viz_bars.frag.qsb") Rectangle {
width: 2
height: root.barHeights[index]
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
enabled: root.isPlaying && !CavaService.cavaAvailable
NumberAnimation {
duration: 100
easing.type: Easing.Linear
}
}
}
}
} }
} }
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
@@ -233,19 +234,16 @@ BasePill {
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
Row { RowLayout {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !root.isVerticalOrientation visible: !root.isVerticalOrientation
readonly property real iconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
IconImage { IconImage {
id: horizontalAppIcon id: horizontalAppIcon
width: contentRow.iconSize Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: contentRow.iconSize Layout.preferredHeight: Layout.preferredWidth
anchors.verticalCenter: parent.verticalCenter
visible: root.showIcon && activeWindow && status === Image.Ready visible: root.showIcon && activeWindow && status === Image.Ready
source: { source: {
if (!activeWindow || !activeWindow.appId) if (!activeWindow || !activeWindow.appId)
@@ -266,10 +264,8 @@ BasePill {
} }
DankIcon { DankIcon {
id: horizontalSteamIcon Layout.preferredWidth: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
width: contentRow.iconSize size: Layout.preferredWidth
size: contentRow.iconSize
anchors.verticalCenter: parent.verticalCenter
name: "sports_esports" name: "sports_esports"
color: Theme.widgetTextColor color: Theme.widgetTextColor
visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId) visible: root.showIcon && activeWindow && activeWindow.appId && horizontalAppIcon.status !== Image.Ready && Paths.isSteamApp(activeWindow.appId)
@@ -284,11 +280,9 @@ BasePill {
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180) Layout.maximumWidth: compactMode ? 80 : 180
visible: text.length > 0 visible: text.length > 0
} }
@@ -297,7 +291,6 @@ BasePill {
text: compactMode ? "" : "•" text: compactMode ? "" : "•"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.outlineButton color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text visible: !compactMode && appText.text && titleText.text
} }
@@ -325,24 +318,9 @@ BasePill {
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
width: { Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
const sp = contentRow.spacing;
let used = 0;
if (horizontalAppIcon.visible)
used += horizontalAppIcon.width + sp;
else if (horizontalSteamIcon.visible)
used += horizontalSteamIcon.width + sp;
if (appText.visible)
used += appText.width + sp;
if (appSeparator.visible)
used += appSeparator.width + sp;
const budget = root.maxWidth - root.horizontalPadding * 2 - used;
return Math.min(implicitWidth, Math.max(0, budget));
}
visible: text.length > 0 visible: text.length > 0
} }
} }
@@ -182,7 +182,7 @@ Item {
function mapWorkspace(ws) { function mapWorkspace(ws) {
return { return {
"num": ws.number, "num": ws.number,
"name": stripSwayWorkspaceNumber(ws.number, ws.name), "name": ws.name,
"focused": ws.focused, "focused": ws.focused,
"active": ws.active, "active": ws.active,
"urgent": ws.urgent, "urgent": ws.urgent,
@@ -202,18 +202,6 @@ Item {
]; ];
} }
// sway/scroll fold `<num>:<name>` into the name field (num 1 name "1:test"); drop the redundant prefix so the index option controls it
function stripSwayWorkspaceNumber(num, name) {
if (num === undefined || num === -1)
return name;
if (typeof name !== "string")
return name;
const prefix = num + ":";
if (!name.startsWith(prefix))
return name;
return name.slice(prefix.length);
}
// Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name // Numbered workspaces first in ascending order; purely-named workspaces (sway reports num -1) after, by name
function swayWorkspaceOrder(a, b) { function swayWorkspaceOrder(a, b) {
const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num; const keyA = a.num === -1 ? Number.MAX_SAFE_INTEGER : a.num;
@@ -240,10 +240,8 @@ DankPopout {
Connections { Connections {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (!root.shouldBeVisible) if (root.shouldBeVisible)
return; mainContainer.forceActiveFocus();
mainContainer.forceActiveFocus();
tabBar.snapIndicator();
} }
} }
@@ -413,7 +411,6 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
active: root.currentTabId === "media" active: root.currentTabId === "media"
visible: active visible: active
asynchronous: true
sourceComponent: Component { sourceComponent: Component {
MediaPlayerTab { MediaPlayerTab {
targetScreen: root.screen targetScreen: root.screen
@@ -465,7 +462,7 @@ DankPopout {
DankSpinner { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
size: 40 size: 40
visible: (wallpaperLoader.active && wallpaperLoader.status === Loader.Loading) || (mediaLoader.active && mediaLoader.status === Loader.Loading) || (weatherLoader.active && weatherLoader.status === Loader.Loading) visible: wallpaperLoader.active && wallpaperLoader.status === Loader.Loading
} }
Loader { Loader {
@@ -473,7 +470,6 @@ DankPopout {
anchors.fill: parent anchors.fill: parent
active: root.currentTabId === "weather" active: root.currentTabId === "weather"
visible: active visible: active
asynchronous: true
sourceComponent: Component { sourceComponent: Component {
WeatherTab {} WeatherTab {}
} }
@@ -44,8 +44,6 @@ Item {
property int __panelHoverCount: 0 property int __panelHoverCount: 0
readonly property bool overlayBlurActive: dropdownBlur.active
onDropdownTypeChanged: { onDropdownTypeChanged: {
if (dropdownType === 0) { if (dropdownType === 0) {
__panelHoverCount = 0; __panelHoverCount = 0;
@@ -77,10 +75,8 @@ Item {
} }
WindowBlur { WindowBlur {
id: dropdownBlur
targetWindow: root.targetWindow targetWindow: root.targetWindow
readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0 readonly property bool active: root.__activePanel !== null && root.__activePanel.visible && root.__activePanel.opacity > 0
blurEnabled: active && Theme.connectedSurfaceBlurEnabled
readonly property real s: root.__activePanel ? Math.min(1, root.__activePanel.scale) : 1 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 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 blurY: root.__activePanel ? root.__activePanel.y + root.__activePanel.height * (1 - s) * 0.5 : 0
+54 -100
View File
@@ -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.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -25,11 +24,6 @@ Item {
property string section: "" property string section: ""
property int barPosition: SettingsData.Position.Top property int barPosition: SettingsData.Position.Top
readonly property color accent: MediaAccentService.accent
readonly property color onAccent: MediaAccentService.onAccent
readonly property color accentHover: MediaAccentService.accentHover
readonly property color accentPressed: MediaAccentService.accentPressed
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge) signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players) signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
@@ -76,7 +70,7 @@ Item {
property bool _switchHold: false property bool _switchHold: false
Timer { Timer {
id: _switchHoldTimer id: _switchHoldTimer
interval: 1500 interval: 650
repeat: false repeat: false
onTriggered: _switchHold = false onTriggered: _switchHold = false
} }
@@ -84,8 +78,7 @@ Item {
onActivePlayerChanged: { onActivePlayerChanged: {
if (!activePlayer) { if (!activePlayer) {
isSwitching = false; isSwitching = false;
_switchHold = true; _switchHold = false;
_switchHoldTimer.restart();
return; return;
} }
isSwitching = true; isSwitching = true;
@@ -124,10 +117,14 @@ Item {
Connections { Connections {
target: MprisController target: MprisController
function onAvailablePlayersChanged() { function onAvailablePlayersChanged() {
if ((MprisController.availablePlayers?.length || 0) === 0) const count = (MprisController.availablePlayers?.length || 0);
if (count === 0) {
isSwitching = false; isSwitching = false;
_switchHold = true; _switchHold = false;
_switchHoldTimer.restart(); } else {
_switchHold = true;
_switchHoldTimer.restart();
}
} }
} }
@@ -293,72 +290,34 @@ Item {
Item { Item {
id: bgContainer id: bgContainer
anchors.fill: parent anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== ""
readonly property string curArt: TrackArtService.resolvedArtUrl Image {
// Two layers crossfade: new art loads into the hidden one and fades in once decoded. id: bgImage
property bool _showA: true anchors.centerIn: parent
visible: layerA.ready || layerB.ready width: Math.max(parent.width, parent.height) * 1.1
height: width
onCurArtChanged: syncArt() source: TrackArtService.resolvedArtUrl
Component.onCompleted: syncArt() fillMode: Image.PreserveAspectCrop
asynchronous: true
function syncArt() { cache: true
if (curArt === "") visible: false
return; onStatusChanged: {
const frontArt = _showA ? layerA.art : layerB.art; if (status === Image.Ready)
const backArt = _showA ? layerB.art : layerA.art; maybeFinishSwitch();
if (frontArt == curArt)
return;
if (backArt == curArt) {
_showA = !_showA;
return;
} }
if (_showA)
layerB.art = curArt;
else
layerA.art = curArt;
} }
component BgBlurLayer: ClippingRectangle { Item {
id: layer id: blurredBg
property alias art: layerImg.source
readonly property bool ready: layerImg.status === Image.Ready && layerImg.source != ""
property bool front: false
signal loaded
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius visible: false
color: "transparent"
antialiasing: true
opacity: front ? 0.7 : 0
Behavior on opacity {
NumberAnimation {
duration: 350
easing.type: Easing.InOutQuad
}
}
Image {
id: layerImg
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: false
onStatusChanged: {
if (status === Image.Ready && source != "")
layer.loaded();
}
}
MultiEffect { MultiEffect {
anchors.centerIn: parent anchors.centerIn: parent
width: layerImg.width width: bgImage.width
height: layerImg.height height: bgImage.height
source: layerImg source: bgImage
blurEnabled: true blurEnabled: true
blurMax: 64 blurMax: 64
blur: 0.8 blur: 0.8
@@ -367,26 +326,22 @@ Item {
} }
} }
BgBlurLayer { Rectangle {
id: layerA id: bgMask
front: bgContainer._showA anchors.fill: parent
onLoaded: { radius: Theme.cornerRadius
if (!bgContainer._showA) { visible: false
bgContainer._showA = true; layer.enabled: true
root.maybeFinishSwitch();
}
}
} }
BgBlurLayer { MultiEffect {
id: layerB anchors.fill: parent
front: !bgContainer._showA source: blurredBg
onLoaded: { maskEnabled: true
if (bgContainer._showA) { maskSource: bgMask
bgContainer._showA = false; maskThresholdMin: 0.5
root.maybeFinishSwitch(); maskSpreadAtMin: 1.0
} opacity: 0.7
}
} }
Rectangle { Rectangle {
@@ -487,8 +442,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
maximumLineCount: 1 maximumLineCount: 1
// Reserve the line so late album metadata doesn't shift the seekbar. visible: text.length > 0
height: Math.max(implicitHeight, Theme.fontSizeSmall * 1.4)
} }
} }
@@ -577,13 +531,13 @@ Item {
height: 40 height: 40
radius: 20 radius: 20
anchors.centerIn: parent anchors.centerIn: parent
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0) color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "shuffle" name: "shuffle"
size: 20 size: 20
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -639,13 +593,13 @@ Item {
height: 50 height: 50
radius: 25 radius: 25
anchors.centerIn: parent anchors.centerIn: parent
color: root.accent color: Theme.primary
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 28 size: 28
color: root.onAccent color: Theme.background
weight: 500 weight: 500
} }
@@ -709,7 +663,7 @@ Item {
height: 40 height: 40
radius: 20 radius: 20
anchors.centerIn: parent anchors.centerIn: parent
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0) color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -726,7 +680,7 @@ Item {
} }
} }
size: 20 size: 20
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -765,7 +719,7 @@ Item {
radius: 20 radius: 20
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 ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
border.color: Theme.outlineStrong border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
@@ -832,7 +786,7 @@ Item {
radius: 20 radius: 20
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 ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
border.width: 1 border.width: 1
z: 101 z: 101
@@ -844,7 +798,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
name: getVolumeIcon() name: getVolumeIcon()
size: 18 size: 18
color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5) color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
} }
MouseArea { MouseArea {
@@ -895,7 +849,7 @@ Item {
radius: 20 radius: 20
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 ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0) color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
border.color: Theme.outlineStrong border.color: Theme.outlineStrong
border.width: 1 border.width: 1
z: 100 z: 100
@@ -154,13 +154,13 @@ Card {
width: 32 width: 32
height: 32 height: 32
radius: 16 radius: 16
color: MediaAccentService.accent color: Theme.primary
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow" name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16 size: 16
color: MediaAccentService.onAccent color: Theme.background
} }
MouseArea { MouseArea {
+6 -17
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modules.DankDash.Overview import qs.Modules.DankDash.Overview
import qs.Widgets
Item { Item {
id: root id: root
@@ -18,7 +17,7 @@ Item {
signal navFocusRequested signal navFocusRequested
function handleKeyEvent(event) { function handleKeyEvent(event) {
return calendarLoader.item ? calendarLoader.item.handleKeyEvent(event) : false; return calendarCard.handleKeyEvent(event);
} }
Item { Item {
@@ -58,26 +57,16 @@ Item {
height: 220 height: 220
} }
// Calendar - bottom middle; deferred so the grid stays off the emerge frame. // Calendar - bottom middle (wider and taller)
Loader { CalendarOverviewCard {
id: calendarLoader id: calendarCard
x: parent.width * 0.2 - Theme.spacingM x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM y: 100 + Theme.spacingM
width: parent.width * 0.6 width: parent.width * 0.6
height: 300 height: 300
asynchronous: true
sourceComponent: Component {
CalendarOverviewCard {
onCloseDash: root.closeDash()
onNavFocusRequested: root.navFocusRequested()
}
}
DankSpinner { onCloseDash: root.closeDash()
anchors.centerIn: parent onNavFocusRequested: root.navFocusRequested()
size: 32
visible: calendarLoader.status === Loader.Loading
}
} }
// Media - bottom right (narrow and taller) // Media - bottom right (narrow and taller)
+32 -2
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Effects
import QtQuick.Shapes import QtQuick.Shapes
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -244,6 +245,17 @@ Item {
size: Theme.iconSize * 2 size: Theme.iconSize * 2
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
}
} }
Column { Column {
@@ -808,6 +820,16 @@ Item {
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false) property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
}
} }
DankIcon { DankIcon {
@@ -820,6 +842,16 @@ Item {
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true) property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
layer.enabled: Theme.elevationEnabled
layer.effect: MultiEffect {
shadowEnabled: Theme.elevationEnabled
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
blurMax: Theme.elevationBlurMax
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
}
} }
} }
} }
@@ -961,7 +993,6 @@ Item {
ListView { ListView {
id: hourlyList id: hourlyList
anchors.fill: parent anchors.fill: parent
reuseItems: true
orientation: ListView.Horizontal orientation: ListView.Horizontal
spacing: Theme.spacingS spacing: Theme.spacingS
clip: true clip: true
@@ -1048,7 +1079,6 @@ Item {
ListView { ListView {
id: dailyList id: dailyList
anchors.fill: parent anchors.fill: parent
reuseItems: true
orientation: ListView.Horizontal orientation: ListView.Horizontal
spacing: Theme.spacingS spacing: Theme.spacingS
clip: true clip: true
+31 -5
View File
@@ -1,7 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell.Widgets import QtQuick.Effects
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -567,11 +567,24 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
ClippingRectangle { Rectangle {
id: gridProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: "transparent" visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: gridButtonRect.isHolding visible: gridButtonRect.isHolding
layer.enabled: gridButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: gridProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
@@ -687,11 +700,24 @@ Rectangle {
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0) border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
ClippingRectangle { Rectangle {
id: listProgressMask
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: "transparent" visible: false
layer.enabled: true
}
Item {
anchors.fill: parent
visible: listButtonRect.isHolding visible: listButtonRect.isHolding
layer.enabled: listButtonRect.isHolding
layer.effect: MultiEffect {
maskEnabled: true
maskSource: listProgressMask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Rectangle { Rectangle {
anchors.left: parent.left anchors.left: parent.left
+3 -78
View File
@@ -13,19 +13,12 @@ Column {
property int draggedIndex: -1 property int draggedIndex: -1
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
property int editingIndex: -1 readonly property real tabItemSize: 128 + Theme.spacingXS
readonly property real tabItemSize: tabRow.dynamicTabWidth + Theme.spacingXS
signal tabSwitched(int tabIndex) signal tabSwitched(int tabIndex)
signal tabClosed(int tabIndex) signal tabClosed(int tabIndex)
signal newTabRequested signal newTabRequested
function commitRename(index, newTitle) {
if (index >= 0)
NotepadStorageService.renameTab(index, newTitle);
editingIndex = -1;
}
function hasUnsavedChangesForTab(tab) { function hasUnsavedChangesForTab(tab) {
if (!tab) if (!tab)
return false; return false;
@@ -39,19 +32,11 @@ Column {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Row { Row {
id: tabRow
width: parent.width width: parent.width
height: 36 height: 36
spacing: Theme.spacingXS spacing: Theme.spacingXS
readonly property real dynamicTabWidth: {
var count = Math.max(1, NotepadStorageService.tabs.length);
var raw = (tabScroll.width - (count - 1) * Theme.spacingXS) / count;
return Math.max(128, Math.min(300, raw));
}
ScrollView { ScrollView {
id: tabScroll
width: parent.width - newTabButton.width - Theme.spacingXS width: parent.width - newTabButton.width - Theme.spacingXS
height: parent.height height: parent.height
clip: true clip: true
@@ -72,8 +57,7 @@ Column {
readonly property bool isActive: NotepadStorageService.currentTabIndex === index readonly property bool isActive: NotepadStorageService.currentTabIndex === index
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
readonly property bool editing: root.editingIndex === index readonly property real tabWidth: 128
readonly property real tabWidth: tabRow.dynamicTabWidth
property bool longPressing: false property bool longPressing: false
property bool dragging: false property bool dragging: false
property point dragStartPos: Qt.point(0, 0) property point dragStartPos: Qt.point(0, 0)
@@ -154,7 +138,6 @@ Column {
StyledText { StyledText {
id: tabText id: tabText
visible: !delegateItem.editing
width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0) width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0)
text: { text: {
var prefix = ""; var prefix = "";
@@ -172,54 +155,13 @@ Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
TextInput {
id: renameField
visible: delegateItem.editing
enabled: delegateItem.editing
width: parent.width
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
selectionColor: Theme.primary
selectedTextColor: Theme.background
selectByMouse: true
clip: true
onEditingFinished: root.commitRename(index, text)
Keys.onEscapePressed: event => {
text = modelData.title || "Untitled";
root.editingIndex = -1;
event.accepted = true;
}
// A tab switch re-focuses the editor via Qt.callLater; the
// timer fires afterwards so the field keeps focus + selection.
Timer {
id: renameFocusTimer
interval: 20
repeat: false
onTriggered: {
renameField.forceActiveFocus();
renameField.selectAll();
}
}
onVisibleChanged: {
if (!visible)
return;
text = modelData.title || "Untitled";
renameFocusTimer.restart();
}
}
Rectangle { Rectangle {
id: tabCloseButton id: tabCloseButton
width: 20 width: 20
height: 20 height: 20
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0) color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0)
visible: NotepadStorageService.tabs.length > 1 && !delegateItem.editing visible: NotepadStorageService.tabs.length > 1
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
DankIcon { DankIcon {
@@ -253,24 +195,11 @@ Column {
MouseArea { MouseArea {
id: tabMouseArea id: tabMouseArea
anchors.fill: parent anchors.fill: parent
enabled: !delegateItem.editing
hoverEnabled: true hoverEnabled: true
preventStealing: dragging || longPressing preventStealing: dragging || longPressing
cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onDoubleClicked: {
root.tabSwitched(index);
root.editingIndex = index;
}
onExited: tabTooltip.hide()
onContainsMouseChanged: {
if (containsMouse && tabText.truncated)
tabTooltip.show(modelData.title || "Untitled", delegateItem, 0, 0, "bottom");
}
onPressed: mouse => { onPressed: mouse => {
if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) { if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) {
delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y); delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y);
@@ -350,8 +279,4 @@ Column {
onClicked: root.newTabRequested() onClicked: root.newTabRequested()
} }
} }
DankTooltipV2 {
id: tabTooltip
}
} }
+21 -5
View File
@@ -4,7 +4,6 @@ import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Widgets
DankOSD { DankOSD {
id: root id: root
@@ -186,11 +185,10 @@ DankOSD {
visible: false visible: false
} }
ClippingRectangle { Item {
id: blurredBg
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius visible: false
color: "transparent"
opacity: 0.7
MultiEffect { MultiEffect {
anchors.centerIn: parent anchors.centerIn: parent
@@ -205,6 +203,24 @@ DankOSD {
} }
} }
Rectangle {
id: bgMask
anchors.fill: parent
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
MultiEffect {
anchors.fill: parent
source: blurredBg
maskEnabled: true
maskSource: bgMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
opacity: 0.7
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
+31 -20
View File
@@ -1,7 +1,7 @@
import QtCore import QtCore
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -520,27 +520,28 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
ClippingRectangle { Image {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
radius: Theme.cornerRadius - 1 source: {
color: "transparent" var wp = Theme.wallpaperPath;
if (!wp || wp === "" || wp.startsWith("#"))
Image { return "";
anchors.fill: parent if (wp.startsWith("file://"))
source: { wp = wp.substring(7);
var wp = Theme.wallpaperPath; return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
if (!wp || wp === "" || wp.startsWith("#")) }
return ""; fillMode: Image.PreserveAspectCrop
if (wp.startsWith("file://")) visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
wp = wp.substring(7); sourceSize.width: 120
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); sourceSize.height: 120
} asynchronous: true
fillMode: Image.PreserveAspectCrop layer.enabled: true
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#") layer.effect: MultiEffect {
sourceSize.width: 120 maskEnabled: true
sourceSize.height: 120 maskSource: autoWallpaperMask
asynchronous: true maskThresholdMin: 0.5
maskSpreadAtMin: 1
} }
} }
@@ -552,6 +553,16 @@ Item {
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#") visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
} }
Rectangle {
id: autoWallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette" name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
+97 -64
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -75,27 +75,28 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
ClippingRectangle { Image {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
radius: Theme.cornerRadius - 1 source: {
color: "transparent" var wp = root.currentWallpaper;
if (wp === "" || wp.startsWith("#"))
Image { return "";
anchors.fill: parent if (wp.startsWith("file://"))
source: { wp = wp.substring(7);
var wp = root.currentWallpaper; return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
if (wp === "" || wp.startsWith("#")) }
return ""; fillMode: Image.PreserveAspectCrop
if (wp.startsWith("file://")) visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
wp = wp.substring(7); sourceSize.width: 160
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); sourceSize.height: 160
} asynchronous: true
fillMode: Image.PreserveAspectCrop layer.enabled: true
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") layer.effect: MultiEffect {
sourceSize.width: 160 maskEnabled: true
sourceSize.height: 160 maskSource: wallpaperMask
asynchronous: true maskThresholdMin: 0.5
maskSpreadAtMin: 1
} }
} }
@@ -107,6 +108,16 @@ Item {
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#") visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
} }
Rectangle {
id: wallpaperMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "image" name: "image"
@@ -410,30 +421,31 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
ClippingRectangle { Image {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
radius: Theme.cornerRadius - 1 source: {
color: "transparent" var wp = SessionData.wallpaperPathLight;
if (wp === "" || wp.startsWith("#"))
Image { return "";
anchors.fill: parent if (wp.startsWith("file://"))
source: { wp = wp.substring(7);
var wp = SessionData.wallpaperPathLight; return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
if (wp === "" || wp.startsWith("#")) }
return ""; fillMode: Image.PreserveAspectCrop
if (wp.startsWith("file://")) visible: {
wp = wp.substring(7); var lightWallpaper = SessionData.wallpaperPathLight;
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
} }
fillMode: Image.PreserveAspectCrop sourceSize.width: 160
visible: { sourceSize.height: 160
var lightWallpaper = SessionData.wallpaperPathLight; asynchronous: true
return lightWallpaper !== "" && !lightWallpaper.startsWith("#"); layer.enabled: true
} layer.effect: MultiEffect {
sourceSize.width: 160 maskEnabled: true
sourceSize.height: 160 maskSource: lightMask
asynchronous: true maskThresholdMin: 0.5
maskSpreadAtMin: 1
} }
} }
@@ -451,6 +463,16 @@ Item {
} }
} }
Rectangle {
id: lightMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "light_mode" name: "light_mode"
@@ -589,30 +611,31 @@ Item {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceVariant color: Theme.surfaceVariant
ClippingRectangle { Image {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
radius: Theme.cornerRadius - 1 source: {
color: "transparent" var wp = SessionData.wallpaperPathDark;
if (wp === "" || wp.startsWith("#"))
Image { return "";
anchors.fill: parent if (wp.startsWith("file://"))
source: { wp = wp.substring(7);
var wp = SessionData.wallpaperPathDark; return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
if (wp === "" || wp.startsWith("#")) }
return ""; fillMode: Image.PreserveAspectCrop
if (wp.startsWith("file://")) visible: {
wp = wp.substring(7); var darkWallpaper = SessionData.wallpaperPathDark;
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/'); return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
} }
fillMode: Image.PreserveAspectCrop sourceSize.width: 160
visible: { sourceSize.height: 160
var darkWallpaper = SessionData.wallpaperPathDark; asynchronous: true
return darkWallpaper !== "" && !darkWallpaper.startsWith("#"); layer.enabled: true
} layer.effect: MultiEffect {
sourceSize.width: 160 maskEnabled: true
sourceSize.height: 160 maskSource: darkMask
asynchronous: true maskThresholdMin: 0.5
maskSpreadAtMin: 1
} }
} }
@@ -630,6 +653,16 @@ Item {
} }
} }
Rectangle {
id: darkMask
anchors.fill: parent
anchors.margins: 1
radius: Theme.cornerRadius - 1
color: "black"
visible: false
layer.enabled: true
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "dark_mode" name: "dark_mode"
+5 -53
View File
@@ -141,37 +141,16 @@ Variants {
function invalidate() { function invalidate() {
_settleFrames = 3; _settleFrames = 3;
backingWindow?.update(); backingWindow?.update();
if (!_wedgeBounced)
wedgeWatchdog.restart();
} }
onRenderActiveChanged: invalidate() onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate() onBackingWindowChanged: invalidate()
// No swap after a requested frame: dropped frame callback left the window
// unexposed (qtwayland mFrameCallbackTimedOut); only surface re-attach recovers.
property bool _wedgeBounced: false
Timer {
id: wedgeWatchdog
interval: 3000
repeat: false
onTriggered: {
if (!root.backingWindow || !wallpaperWindow.visible || IdleService.isShellLocked)
return;
log.warn("no frame swapped on", modelData.name, "since last invalidate, re-attaching surface");
root._wedgeBounced = true;
surfaceReattach.restart();
}
}
Connections { Connections {
target: root.backingWindow target: root.backingWindow
function onFrameSwapped() { function onFrameSwapped() {
if (root._settleFrames > 0) if (root._settleFrames > 0)
root._settleFrames--; root._settleFrames--;
root._wedgeBounced = false;
wedgeWatchdog.stop();
} }
function onVisibleChanged() { function onVisibleChanged() {
root.invalidate(); root.invalidate();
@@ -197,29 +176,10 @@ Variants {
function onWallpaperFillModeChanged() { function onWallpaperFillModeChanged() {
root.invalidate(); root.invalidate();
} }
function onEffectiveWallpaperBackgroundColorChanged() { function onWallpaperBackgroundColorModeChanged() {
root.invalidate(); root.invalidate();
} }
} function onWallpaperBackgroundCustomColorChanged() {
Connections {
target: SessionData
function onMonitorWallpaperFillModesChanged() {
root.invalidate();
}
function onPerMonitorWallpaperChanged() {
root.invalidate();
}
}
// Theme changes repaint DankBackdrop but nothing else wakes the render loop
Connections {
target: Theme
enabled: root.isColorSource || currentWallpaper.status === Image.Error
function onPrimaryChanged() {
root.invalidate();
}
function onBackgroundChanged() {
root.invalidate(); root.invalidate();
} }
} }
@@ -542,13 +502,13 @@ Variants {
} }
onSourceChanged: { onSourceChanged: {
invalidate();
if (!source || source.startsWith("#")) { if (!source || source.startsWith("#")) {
setWallpaperImmediate(""); setWallpaperImmediate("");
return; return;
} }
root.changePending = true; root.changePending = true;
invalidate();
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source); const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
@@ -568,14 +528,10 @@ Variants {
} }
function setWallpaperImmediate(newSource) { function setWallpaperImmediate(newSource) {
transitionDelayTimer.stop();
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
root.screenScale = CompositorService.getScreenScale(modelData); root.screenScale = CompositorService.getScreenScale(modelData);
// No status change coming to clear the flag
if (!newSource || currentWallpaper.source.toString() === newSource)
root.changePending = false;
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
@@ -611,14 +567,10 @@ Variants {
} }
function changeWallpaper(newPath, force) { function changeWallpaper(newPath, force) {
if (!force && newPath === currentWallpaper.source.toString()) { if (!force && newPath === currentWallpaper.source)
root.changePending = false;
return; return;
} if (!newPath || newPath.startsWith("#"))
if (!newPath || newPath.startsWith("#")) {
root.changePending = false;
return; return;
}
root.screenScale = CompositorService.getScreenScale(modelData); root.screenScale = CompositorService.getScreenScale(modelData);
if (root.transitioning || root.effectActive) { if (root.transitioning || root.effectActive) {
root.pendingWallpaper = newPath; root.pendingWallpaper = newPath;
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
Item { Item {
@@ -58,6 +58,23 @@ Item {
visible: intersectsViewport visible: intersectsViewport
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4 opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
Rectangle {
id: maskRect
width: root.width
height: root.height
radius: Theme.cornerRadius
visible: false
layer.enabled: true
}
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: maskRect
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
@@ -87,55 +104,51 @@ Item {
} }
} }
ClippingRectangle { ScreencopyView {
id: windowPreview
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius captureSource: root.overviewOpen ? root.toplevel?.wayland : null
color: "transparent" live: true
ScreencopyView { Rectangle {
id: windowPreview
anchors.fill: parent anchors.fill: parent
captureSource: root.overviewOpen ? root.toplevel?.wayland : null radius: Theme.cornerRadius
live: true color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
Theme.withAlpha(Theme.surfaceContainer, 0.1)
border.color: Theme.withAlpha(Theme.outline, 0.3)
border.width: 1
}
Rectangle { ColumnLayout {
anchors.fill: parent anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius anchors.left: parent.left
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1) anchors.right: parent.right
border.color: Theme.withAlpha(Theme.outline, 0.3) spacing: Theme.fontSizeSmall * 0.5
border.width: 1
}
ColumnLayout { Image {
anchors.verticalCenter: parent.verticalCenter id: windowIcon
anchors.left: parent.left property var iconSize: {
anchors.right: parent.right return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
spacing: Theme.fontSizeSmall * 0.5 }
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Image { Behavior on width {
id: windowIcon NumberAnimation {
property var iconSize: { duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1); easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
} }
Layout.alignment: Qt.AlignHCenter }
source: root.iconPath Behavior on height {
width: iconSize NumberAnimation {
height: iconSize duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
sourceSize: Qt.size(iconSize, iconSize) easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
}
} }
} }
} }
+1 -5
View File
@@ -298,11 +298,7 @@ Singleton {
function getBuiltInLauncherItems(pluginId, query) { function getBuiltInLauncherItems(pluginId, query) {
if (pluginId === "dms_clipboard_search") { if (pluginId === "dms_clipboard_search") {
const trimmed = (query || "").toString().trim(); const trimmed = (query || "").toString().trim();
const entries = ClipboardService.getCachedLauncherSearchEntries(trimmed, 20).slice().sort((a, b) => { const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return entries.map(entry => ({ return entries.map(entry => ({
type: "clipboard", type: "clipboard",
data: entry data: entry
+19 -22
View File
@@ -841,32 +841,13 @@ EOFCONFIG
function setVolume(percentage) { function setVolume(percentage) {
if (!root.sink?.audio) if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
if (isNaN(percentage))
return "Invalid percentage";
const maxVol = root.sinkMaxVolume; const maxVol = root.sinkMaxVolume;
const clampedVolume = Math.max(0, Math.min(maxVol, percentage)); const clampedVolume = Math.max(0, Math.min(maxVol, percentage));
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", String(clampedVolume / 100)]); root.sink.audio.volume = clampedVolume / 100;
return `Volume set to ${clampedVolume}%`; return `Volume set to ${clampedVolume}%`;
} }
function outputVolumeStep(step) {
const parsed = parseInt(step || "5");
return isNaN(parsed) ? 5 : Math.max(0, parsed);
}
function adjustDefaultSinkVolume(step, direction) {
const maxVol = root.sinkMaxVolume;
const stepValue = outputVolumeStep(step);
const currentVolume = Math.round(root.sink.audio.volume * 100);
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + direction * stepValue));
const suffix = direction > 0 ? "+" : "-";
Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "0"]);
Quickshell.execDetached(["wpctl", "set-volume", "-l", String(maxVol / 100), "@DEFAULT_AUDIO_SINK@", `${stepValue}%${suffix}`]);
return newVolume;
}
function toggleMute() { function toggleMute() {
if (!root.sink?.audio) { if (!root.sink?.audio) {
return "No audio sink available"; return "No audio sink available";
@@ -958,7 +939,15 @@ EOFCONFIG
if (!root.sink?.audio) if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
const newVolume = root.adjustDefaultSinkVolume(step, 1); if (root.sink.audio.muted)
root.sink.audio.muted = false;
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(maxVol, currentVolume + stepValue));
root.sink.audio.volume = newVolume / 100;
return `Volume increased to ${newVolume}%`; return `Volume increased to ${newVolume}%`;
} }
@@ -966,7 +955,15 @@ EOFCONFIG
if (!root.sink?.audio) if (!root.sink?.audio)
return "No audio sink available"; return "No audio sink available";
const newVolume = root.adjustDefaultSinkVolume(step, -1); if (root.sink.audio.muted)
root.sink.audio.muted = false;
const maxVol = root.sinkMaxVolume;
const currentVolume = Math.round(root.sink.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(maxVol, currentVolume - stepValue));
root.sink.audio.volume = newVolume / 100;
return `Volume decreased to ${newVolume}%`; return `Volume decreased to ${newVolume}%`;
} }
+2 -2
View File
@@ -12,8 +12,8 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("ChangelogService") readonly property var log: Log.scoped("ChangelogService")
readonly property string currentVersion: "1.5" readonly property string currentVersion: "1.4"
readonly property bool changelogEnabled: true readonly property bool changelogEnabled: false
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell" readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/DankMaterialShell"
readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion readonly property string changelogMarkerPath: configDir + "/.changelog-" + currentVersion
+84 -19
View File
@@ -30,6 +30,7 @@ Singleton {
property int selectedIndex: 0 property int selectedIndex: 0
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property int refCount: 0 property int refCount: 0
property real _launcherLastRefresh: 0
property bool _launcherCacheValid: false property bool _launcherCacheValid: false
property string _launcherCachedQuery: "" property string _launcherCachedQuery: ""
property var _launcherCachedEntries: [] property var _launcherCachedEntries: []
@@ -84,30 +85,31 @@ Singleton {
} }
function updateFilteredModel() { function updateFilteredModel() {
const query = searchText.trim().toLowerCase(); let filtered = internalEntries;
const filterAll = activeFilter === "all";
const unpinned = [];
const pinned = [];
for (let i = 0; i < internalEntries.length; i++) { if (activeFilter !== "all") {
const entry = internalEntries[i]; filtered = filtered.filter(entry => getEntryType(entry) === activeFilter);
if (!filterAll && getEntryType(entry) !== activeFilter)
continue;
if (query.length > 0 && !entry.preview.toLowerCase().includes(query))
continue;
(entry.pinned ? pinned : unpinned).push(entry);
} }
const byIdDesc = (a, b) => b.id - a.id; const query = searchText.trim();
pinned.sort(byIdDesc);
unpinned.sort(byIdDesc);
pinnedEntries = pinned; if (query.length > 0) {
unpinnedEntries = unpinned; const lowerQuery = query.toLowerCase();
clipboardEntries = pinned.concat(unpinned); filtered = filtered.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
}
filtered.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return b.id - a.id;
});
clipboardEntries = filtered;
unpinnedEntries = filtered.filter(e => !e.pinned);
pinnedEntries = filtered.filter(e => e.pinned);
totalCount = clipboardEntries.length; totalCount = clipboardEntries.length;
const activeCount = Math.max(unpinned.length, pinned.length); const activeCount = Math.max(unpinnedEntries.length, pinnedEntries.length);
if (activeCount === 0) { if (activeCount === 0) {
keyboardNavigationActive = false; keyboardNavigationActive = false;
@@ -115,8 +117,9 @@ Singleton {
return; return;
} }
if (selectedIndex >= activeCount) if (selectedIndex >= activeCount) {
selectedIndex = activeCount - 1; selectedIndex = activeCount - 1;
}
} }
function refresh() { function refresh() {
@@ -135,6 +138,18 @@ Singleton {
}); });
} }
function ensureLauncherHistory() {
if (!clipboardAvailable) {
return;
}
const now = Date.now();
if (internalEntries.length === 0 || now - _launcherLastRefresh > 5000) {
_launcherLastRefresh = now;
refresh();
}
}
function requestLauncherSearch(query, limit) { function requestLauncherSearch(query, limit) {
if (!clipboardAvailable) { if (!clipboardAvailable) {
return; return;
@@ -192,6 +207,56 @@ Singleton {
_launcherSearchSeq++; _launcherSearchSeq++;
} }
function getLauncherEntries(query, limit, minLength) {
if (!clipboardAvailable) {
return [];
}
const trimmed = (query || "").toString().trim();
const requiredLength = minLength !== undefined ? minLength : 2;
if (trimmed.length < requiredLength) {
return [];
}
const lowerQuery = trimmed.toLowerCase();
const maxItems = limit > 0 ? limit : 8;
const matches = [];
for (var i = 0; i < internalEntries.length; i++) {
const entry = internalEntries[i];
const preview = getEntryPreview(entry).toString();
const typeText = entry.isImage ? "image picture screenshot clipboard" : "text clipboard";
const haystack = (preview + " " + typeText).toLowerCase();
if (haystack.indexOf(lowerQuery) === -1) {
continue;
}
matches.push(entry);
}
matches.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return matches.slice(0, maxItems);
}
function getRecentLauncherEntries(limit) {
if (!clipboardAvailable) {
return [];
}
const maxItems = limit > 0 ? limit : 20;
const entries = internalEntries.slice();
entries.sort((a, b) => {
if (a.pinned !== b.pinned)
return b.pinned ? 1 : -1;
return (b.id || 0) - (a.id || 0);
});
return entries.slice(0, maxItems);
}
function reset() { function reset() {
searchText = ""; searchText = "";
selectedIndex = 0; selectedIndex = 0;
@@ -1,76 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Common
import qs.Services
// Accent color extracted from the current track's album art via ColorQuantizer,
// falling back to Theme.primary when no usable accent is available.
Singleton {
id: root
readonly property bool hasAccent: _accent !== null
readonly property color accent: _accent !== null ? _accent : Theme.primary
readonly property color onAccent: {
const c = accent;
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
}
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
readonly property color accentTrack: Theme.withAlpha(accent, 0.28)
readonly property color accentSubtle: Theme.withAlpha(accent, 0.55)
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
readonly property string artUrl: TrackArtService.resolvedArtUrl
onArtUrlChanged: {
if (artUrl === "")
_accent = null;
}
property var _accent: null
ColorQuantizer {
id: quantizer
source: root.artUrl
depth: 4
rescaleSize: 64
onColorsChanged: root._accent = root._pickAccent(colors)
}
function _pickAccent(colors) {
if (!colors || colors.length === 0)
return null;
let best = null;
let bestScore = -1;
for (let i = 0; i < colors.length; i++) {
const c = colors[i];
const s = c.hsvSaturation;
const v = c.hsvValue;
if (v < 0.22 || v > 0.96 || s < 0.22)
continue;
const score = s * (1 - Math.abs(v - 0.68));
if (score > bestScore) {
bestScore = score;
best = c;
}
}
if (!best)
return null;
return _normalize(best);
}
function _normalize(c) {
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
const s = Math.min(1, c.hsvSaturation * 1.05);
const v = Math.max(c.hsvValue, 0.62);
return Qt.hsva(hue, s, v, 1);
}
}
+275 -226
View File
@@ -29,48 +29,45 @@ Singleton {
function setSessionBuffer(tabId, content, baseline) { function setSessionBuffer(tabId, content, baseline) {
if (tabId === undefined || tabId === null || tabId < 0) if (tabId === undefined || tabId === null || tabId < 0)
return; return
var next = Object.assign({}, sessionBuffers); var next = Object.assign({}, sessionBuffers)
if (content !== baseline) { if (content !== baseline) {
next[tabId] = { next[tabId] = { content: content, baseline: baseline }
content: content,
baseline: baseline
};
} else { } else {
delete next[tabId]; delete next[tabId]
} }
sessionBuffers = next; sessionBuffers = next
sessionBufferRevision++; sessionBufferRevision++
} }
function getSessionBuffer(tabId) { function getSessionBuffer(tabId) {
return sessionBuffers[tabId]; return sessionBuffers[tabId]
} }
function clearSessionBuffer(tabId) { function clearSessionBuffer(tabId) {
if (sessionBuffers[tabId] === undefined) if (sessionBuffers[tabId] === undefined)
return; return
var next = Object.assign({}, sessionBuffers); var next = Object.assign({}, sessionBuffers)
delete next[tabId]; delete next[tabId]
sessionBuffers = next; sessionBuffers = next
sessionBufferRevision++; sessionBufferRevision++
} }
property var conflictTabId: -1 property var conflictTabId: -1
property string conflictDiskContent: "" property string conflictDiskContent: ""
function flagConflict(tabId, diskContent) { function flagConflict(tabId, diskContent) {
conflictDiskContent = diskContent; conflictDiskContent = diskContent
conflictTabId = tabId; conflictTabId = tabId
} }
function clearConflict() { function clearConflict() {
conflictTabId = -1; conflictTabId = -1
conflictDiskContent = ""; conflictDiskContent = ""
} }
Component.onCompleted: { Component.onCompleted: {
ensureDirectories(); ensureDirectories()
} }
FileView { FileView {
@@ -81,66 +78,64 @@ Singleton {
onLoaded: { onLoaded: {
try { try {
var data = JSON.parse(text()); var data = JSON.parse(text())
root.tabs = data.tabs || []; root.tabs = data.tabs || []
root.currentTabIndex = data.currentTabIndex || 0; root.currentTabIndex = data.currentTabIndex || 0
root.metadataLoaded = true; root.metadataLoaded = true
root.validateTabs(); root.validateTabs()
} catch (e) { } catch(e) {
log.warn("Failed to parse notepad metadata:", e); log.warn("Failed to parse notepad metadata:", e)
root.createDefaultTab(); root.createDefaultTab()
} }
} }
onLoadFailed: { onLoadFailed: {
root.createDefaultTab(); root.createDefaultTab()
} }
} }
onRefCountChanged: { onRefCountChanged: {
if (refCount === 1 && !metadataLoaded) { if (refCount === 1 && !metadataLoaded) {
metadataFile.path = ""; metadataFile.path = ""
metadataFile.path = root.metadataPath; metadataFile.path = root.metadataPath
} }
} }
function ensureDirectories() { function ensureDirectories() {
Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null); mkdirProcess.running = true
} }
function loadMetadata() { function loadMetadata() {
metadataFile.path = ""; metadataFile.path = ""
metadataFile.path = root.metadataPath; metadataFile.path = root.metadataPath
} }
function createDefaultTab() { function createDefaultTab() {
var id = Date.now(); var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"; var filePath = "notepad-files/untitled-" + id + ".txt"
var fullPath = baseDir + "/" + filePath; var fullPath = baseDir + "/" + filePath
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated); var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true; newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated; tabsBeingCreated = newTabsBeingCreated
root.createEmptyFile(fullPath, function () { root.createEmptyFile(fullPath, function() {
root.tabs = [ root.tabs = [{
{ id: id,
id: id, title: I18n.tr("Untitled"),
title: I18n.tr("Untitled"), filePath: filePath,
filePath: filePath, isTemporary: true,
isTemporary: true, lastModified: new Date().toISOString(),
lastModified: new Date().toISOString(), cursorPosition: 0,
cursorPosition: 0, scrollPosition: 0
scrollPosition: 0 }]
} root.currentTabIndex = 0
];
root.currentTabIndex = 0;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated); var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]; delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated; tabsBeingCreated = updatedTabsBeingCreated
root.saveMetadata(); root.saveMetadata()
}); })
} }
function saveMetadata() { function saveMetadata() {
@@ -148,73 +143,82 @@ Singleton {
version: 1, version: 1,
currentTabIndex: currentTabIndex, currentTabIndex: currentTabIndex,
tabs: tabs tabs: tabs
}; }
metadataFile.setText(JSON.stringify(metadata, null, 2)); metadataFile.setText(JSON.stringify(metadata, null, 2))
} }
function getTabById(tabId) { function getTabById(tabId) {
for (var i = 0; i < tabs.length; i++) { for (var i = 0; i < tabs.length; i++) {
if (tabs[i].id === tabId) if (tabs[i].id === tabId)
return tabs[i]; return tabs[i]
} }
return null; return null
} }
function loadTabContent(tabIndex, callback) { function loadTabContent(tabIndex, callback) {
if (tabIndex < 0 || tabIndex >= tabs.length) { if (tabIndex < 0 || tabIndex >= tabs.length) {
callback(""); callback("")
return; return
} }
var tab = tabs[tabIndex]; var tab = tabs[tabIndex]
var requestTabId = tab.id; var requestTabId = tab.id
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath; var fullPath = tab.isTemporary
? baseDir + "/" + tab.filePath
: tab.filePath
if (tabsBeingCreated[tab.id]) { if (tabsBeingCreated[tab.id]) {
Qt.callLater(() => { Qt.callLater(() => {
loadTabContent(tabIndex, callback); loadTabContent(tabIndex, callback)
}); })
return; return
} }
Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => { var fileChecker = fileExistsComponent.createObject(root, {
var currentTab = root.getTabById(requestTabId); path: fullPath,
var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : ""; callback: (exists) => {
var currentTab = root.getTabById(requestTabId)
var currentPath = currentTab
? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath)
: ""
if (!currentTab || currentPath !== fullPath) { if (!currentTab || currentPath !== fullPath) {
callback(""); callback("")
return; return
} }
if (exitCode === 0) { if (exists) {
tabFileLoaderComponent.createObject(root, { var loader = tabFileLoaderComponent.createObject(root, {
path: fullPath, path: fullPath,
callback: callback callback: callback
}); })
} else { } else {
log.warn("Tab file does not exist:", fullPath); log.warn("Tab file does not exist:", fullPath)
callback(""); callback("")
}
} }
}); })
} }
function saveTabContent(tabIndex, content) { function saveTabContent(tabIndex, content) {
if (tabIndex < 0 || tabIndex >= tabs.length) if (tabIndex < 0 || tabIndex >= tabs.length) return
return;
var tab = tabs[tabIndex]; var tab = tabs[tabIndex]
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath; var fullPath = tab.isTemporary
? baseDir + "/" + tab.filePath
: tab.filePath
var saver = tabFileSaverComponent.createObject(root, { var saver = tabFileSaverComponent.createObject(root, {
path: fullPath, path: fullPath,
content: content, content: content,
tabIndex: tabIndex tabIndex: tabIndex
}); })
} }
function createNewTab() { function createNewTab() {
var id = Date.now(); var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"; var filePath = "notepad-files/untitled-" + id + ".txt"
var fullPath = baseDir + "/" + filePath; var fullPath = baseDir + "/" + filePath
var newTab = { var newTab = {
id: id, id: id,
@@ -224,29 +228,29 @@ Singleton {
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
cursorPosition: 0, cursorPosition: 0,
scrollPosition: 0 scrollPosition: 0
}; }
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated); var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true; newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated; tabsBeingCreated = newTabsBeingCreated
createEmptyFile(fullPath, function () { createEmptyFile(fullPath, function() {
var newTabs = tabs.slice(); var newTabs = tabs.slice()
newTabs.push(newTab); newTabs.push(newTab)
tabs = newTabs; tabs = newTabs
currentTabIndex = tabs.length - 1; currentTabIndex = tabs.length - 1
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated); var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]; delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated; tabsBeingCreated = updatedTabsBeingCreated
saveMetadata(); saveMetadata()
}); })
return newTab; return newTab
} }
function createTabForFile(path) { function createTabForFile(path) {
var id = Date.now(); var id = Date.now()
var fileName = path.split('/').pop(); var fileName = path.split('/').pop()
var newTab = { var newTab = {
id: id, id: id,
@@ -256,34 +260,34 @@ Singleton {
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
cursorPosition: 0, cursorPosition: 0,
scrollPosition: 0 scrollPosition: 0
}; }
var newTabs = tabs.slice(); var newTabs = tabs.slice()
newTabs.push(newTab); newTabs.push(newTab)
tabs = newTabs; tabs = newTabs
currentTabIndex = tabs.length - 1; currentTabIndex = tabs.length - 1
saveMetadata(); saveMetadata()
return newTab; return newTab
} }
function closeTab(tabIndex) { function closeTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length) if (tabIndex < 0 || tabIndex >= tabs.length) return
return;
var newTabs = tabs.slice(); var newTabs = tabs.slice()
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1; var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
clearSessionBuffer(closedTabId); clearSessionBuffer(closedTabId)
if (conflictTabId === closedTabId) if (conflictTabId === closedTabId)
clearConflict(); clearConflict()
if (newTabs.length <= 1) { if (newTabs.length <= 1) {
var id = Date.now(); var id = Date.now()
var filePath = "notepad-files/untitled-" + id + ".txt"; var filePath = "notepad-files/untitled-" + id + ".txt"
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated); var newTabsBeingCreated = Object.assign({}, tabsBeingCreated)
newTabsBeingCreated[id] = true; newTabsBeingCreated[id] = true
tabsBeingCreated = newTabsBeingCreated; tabsBeingCreated = newTabsBeingCreated
createEmptyFile(baseDir + "/" + filePath, function () { createEmptyFile(baseDir + "/" + filePath, function() {
newTabs[0] = { newTabs[0] = {
id: id, id: id,
title: I18n.tr("Untitled"), title: I18n.tr("Untitled"),
@@ -292,129 +296,110 @@ Singleton {
lastModified: new Date().toISOString(), lastModified: new Date().toISOString(),
cursorPosition: 0, cursorPosition: 0,
scrollPosition: 0 scrollPosition: 0
}; }
currentTabIndex = 0; currentTabIndex = 0
tabs = newTabs; tabs = newTabs
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated); var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated)
delete updatedTabsBeingCreated[id]; delete updatedTabsBeingCreated[id]
tabsBeingCreated = updatedTabsBeingCreated; tabsBeingCreated = updatedTabsBeingCreated
saveMetadata(); saveMetadata()
}); })
return; return
} else { } else {
var tabToDelete = newTabs[tabIndex]; var tabToDelete = newTabs[tabIndex]
if (tabToDelete && tabToDelete.isTemporary) { if (tabToDelete && tabToDelete.isTemporary) {
deleteFile(baseDir + "/" + tabToDelete.filePath); deleteFile(baseDir + "/" + tabToDelete.filePath)
} }
newTabs.splice(tabIndex, 1); newTabs.splice(tabIndex, 1)
if (currentTabIndex >= newTabs.length) { if (currentTabIndex >= newTabs.length) {
currentTabIndex = newTabs.length - 1; currentTabIndex = newTabs.length - 1
} else if (currentTabIndex > tabIndex) { } else if (currentTabIndex > tabIndex) {
currentTabIndex -= 1; currentTabIndex -= 1
} }
} }
tabs = newTabs; tabs = newTabs
saveMetadata(); saveMetadata()
} }
function switchToTab(tabIndex) { function switchToTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length) if (tabIndex < 0 || tabIndex >= tabs.length) return
return;
currentTabIndex = tabIndex; currentTabIndex = tabIndex
saveMetadata(); saveMetadata()
} }
function reorderTab(fromIndex, toIndex) { function reorderTab(fromIndex, toIndex) {
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length) if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
return; return
if (fromIndex === toIndex) if (fromIndex === toIndex)
return; return
var newTabs = tabs.slice();
var moved = newTabs.splice(fromIndex, 1)[0]; var newTabs = tabs.slice()
newTabs.splice(toIndex, 0, moved); var moved = newTabs.splice(fromIndex, 1)[0]
tabs = newTabs; newTabs.splice(toIndex, 0, moved)
tabs = newTabs
if (currentTabIndex === fromIndex) { if (currentTabIndex === fromIndex) {
currentTabIndex = toIndex; currentTabIndex = toIndex
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) { } else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
currentTabIndex--; currentTabIndex--
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) { } else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
currentTabIndex++; currentTabIndex++
} }
saveMetadata(); saveMetadata()
} }
function saveTabAs(tabIndex, userPath) { function saveTabAs(tabIndex, userPath) {
if (tabIndex < 0 || tabIndex >= tabs.length) if (tabIndex < 0 || tabIndex >= tabs.length) return
return;
var tab = tabs[tabIndex]; var tab = tabs[tabIndex]
var fileName = userPath.split('/').pop(); var fileName = userPath.split('/').pop()
if (tab.isTemporary) { if (tab.isTemporary) {
var tempPath = baseDir + "/" + tab.filePath; var tempPath = baseDir + "/" + tab.filePath
copyFile(tempPath, userPath); copyFile(tempPath, userPath)
deleteFile(tempPath); deleteFile(tempPath)
} }
var newTabs = tabs.slice(); var newTabs = tabs.slice()
newTabs[tabIndex] = Object.assign({}, tab, { newTabs[tabIndex] = Object.assign({}, tab, {
title: fileName, title: fileName,
filePath: userPath, filePath: userPath,
isTemporary: false, isTemporary: false,
lastModified: new Date().toISOString() lastModified: new Date().toISOString()
}); })
tabs = newTabs; tabs = newTabs
saveMetadata(); saveMetadata()
}
function renameTab(tabIndex, newTitle) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var trimmed = (newTitle || "").trim();
var tab = tabs[tabIndex];
if (trimmed.length === 0 || trimmed === tab.title)
return;
if (tab.isTemporary) {
updateTabMetadata(tabIndex, {
title: trimmed
});
return;
}
var dir = tab.filePath.substring(0, tab.filePath.lastIndexOf('/') + 1);
var newPath = dir + trimmed;
moveFile(tab.filePath, newPath);
updateTabMetadata(tabIndex, {
title: trimmed,
filePath: newPath
});
} }
function updateTabMetadata(tabIndex, properties) { function updateTabMetadata(tabIndex, properties) {
if (tabIndex < 0 || tabIndex >= tabs.length) if (tabIndex < 0 || tabIndex >= tabs.length) return
return;
var newTabs = tabs.slice(); var newTabs = tabs.slice()
var updatedTab = Object.assign({}, newTabs[tabIndex], properties); var updatedTab = Object.assign({}, newTabs[tabIndex], properties)
updatedTab.lastModified = new Date().toISOString(); updatedTab.lastModified = new Date().toISOString()
newTabs[tabIndex] = updatedTab; newTabs[tabIndex] = updatedTab
tabs = newTabs; tabs = newTabs
saveMetadata(); saveMetadata()
} }
function validateTabs() { function validateTabs() {
var validTabs = []; var validTabs = []
for (var i = 0; i < tabs.length; i++) { for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i]; var tab = tabs[i]
validTabs.push(tab); validTabs.push(tab)
} }
tabs = validTabs; tabs = validTabs
if (tabs.length === 0) { if (tabs.length === 0) {
root.createDefaultTab(); root.createDefaultTab()
} }
} }
@@ -426,13 +411,29 @@ Singleton {
preload: true preload: true
onLoaded: { onLoaded: {
callback(text()); callback(text())
destroy(); destroy()
} }
onLoadFailed: { onLoadFailed: {
callback(""); callback("")
destroy(); destroy()
}
}
}
Component {
id: fileExistsComponent
Process {
property string path
property var callback
command: ["test", "-f", path]
Component.onCompleted: running = true
onExited: (exitCode) => {
callback(exitCode === 0)
destroy()
} }
} }
} }
@@ -451,46 +452,94 @@ Singleton {
onSaved: { onSaved: {
if (tabIndex >= 0) { if (tabIndex >= 0) {
root.updateTabMetadata(tabIndex, {}); root.updateTabMetadata(tabIndex, {})
} }
if (creationCallback) { if (creationCallback) {
creationCallback(); creationCallback()
} }
destroy(); destroy()
} }
onSaveFailed: { onSaveFailed: {
log.error("Failed to save tab content"); log.error("Failed to save tab content")
if (creationCallback) { if (creationCallback) {
creationCallback(); creationCallback()
} }
destroy(); destroy()
} }
} }
} }
function createEmptyFile(path, callback) { function createEmptyFile(path, callback) {
var cleanPath = decodeURI(path.toString()); var cleanPath = decodeURI(path.toString())
if (!cleanPath.startsWith("/")) { if (!cleanPath.startsWith("/")) {
cleanPath = baseDir + "/" + cleanPath; cleanPath = baseDir + "/" + cleanPath
} }
Proc.runCommand("", ["touch", cleanPath], (output, exitCode) => { var creator = fileCreatorComponent.createObject(root, {
if (callback) filePath: cleanPath,
callback(); creationCallback: callback
}); })
} }
function copyFile(source, destination) { function copyFile(source, destination) {
Proc.runCommand("", ["cp", source, destination], null); copyProcess.source = source
copyProcess.destination = destination
copyProcess.running = true
} }
function deleteFile(path) { function deleteFile(path) {
Proc.runCommand("", ["rm", "-f", path], null); deleteProcess.filePath = path
deleteProcess.running = true
} }
function moveFile(source, destination) { Component {
Proc.runCommand("", ["mv", source, destination], null); id: fileCreatorComponent
QtObject {
property string filePath
property var creationCallback
Component.onCompleted: {
var touchProcess = touchProcessComponent.createObject(this, {
filePath: filePath,
callback: creationCallback
})
}
}
}
Component {
id: touchProcessComponent
Process {
property string filePath
property var callback
command: ["touch", filePath]
Component.onCompleted: running = true
onExited: (exitCode) => {
if (callback) callback()
destroy()
}
}
}
Process {
id: copyProcess
property string source
property string destination
command: ["cp", source, destination]
}
Process {
id: deleteProcess
property string filePath
command: ["rm", "-f", filePath]
}
Process {
id: mkdirProcess
command: ["mkdir", "-p", root.baseDir, root.filesDir]
} }
} }
+6 -27
View File
@@ -4,7 +4,6 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -851,28 +850,10 @@ Singleton {
} }
} }
function notepadSlideoutForFocusedScreen() {
if (!notepadSlideouts || notepadSlideouts.length === 0)
return null;
const focused = BarWidgetService.getFocusedScreenName();
if (focused) {
for (var i = 0; i < notepadSlideouts.length; i++) {
if (notepadSlideouts[i]?.modelData?.name === focused)
return notepadSlideouts[i];
}
}
return notepadSlideouts[0];
}
// Remembered presentation wins over the configured default until the user
// changes the default in settings (handled below).
readonly property string notepadResolvedMode: SessionData.notepadLastMode || SettingsData.notepadDefaultMode
function openNotepadSlideout() { function openNotepadSlideout() {
SessionData.setNotepadLastMode("slideout");
notepadPopout?.hide(); notepadPopout?.hide();
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideoutForFocusedScreen()?.show(); notepadSlideouts[0]?.show();
} }
} }
@@ -880,7 +861,6 @@ Singleton {
Connections { Connections {
target: SettingsData target: SettingsData
function onNotepadDefaultModeChanged() { function onNotepadDefaultModeChanged() {
SessionData.setNotepadLastMode(SettingsData.notepadDefaultMode);
if (SettingsData.notepadDefaultMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
var hadSlideout = false; var hadSlideout = false;
for (var i = 0; i < root.notepadSlideouts.length; i++) { for (var i = 0; i < root.notepadSlideouts.length; i++) {
@@ -899,7 +879,7 @@ Singleton {
} }
function openNotepad() { function openNotepad() {
if (notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
openNotepadPopout(); openNotepadPopout();
return; return;
} }
@@ -907,22 +887,22 @@ Singleton {
} }
function closeNotepad() { function closeNotepad() {
if (notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
notepadPopout?.hide(); notepadPopout?.hide();
return; return;
} }
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideoutForFocusedScreen()?.hide(); notepadSlideouts[0]?.hide();
} }
} }
function toggleNotepad() { function toggleNotepad() {
if (notepadResolvedMode === "popout") { if (SettingsData.notepadDefaultMode === "popout") {
toggleNotepadPopout(); toggleNotepadPopout();
return; return;
} }
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideoutForFocusedScreen()?.toggle(); notepadSlideouts[0]?.toggle();
} }
} }
@@ -932,7 +912,6 @@ Singleton {
property string _notepadPendingOpenFilePath: "" property string _notepadPendingOpenFilePath: ""
function openNotepadPopout() { function openNotepadPopout() {
SessionData.setNotepadLastMode("popout");
closeNotepadSlideouts(); closeNotepadSlideouts();
if (notepadPopout) { if (notepadPopout) {
notepadPopout.show(); notepadPopout.show();
+5 -34
View File
@@ -14,8 +14,6 @@ Singleton {
property bool hasUwsm: false property bool hasUwsm: false
property bool isElogind: false property bool isElogind: false
property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false
property bool hibernateSupported: false property bool hibernateSupported: false
property bool inhibitorAvailable: true property bool inhibitorAvailable: true
property bool idleInhibited: false property bool idleInhibited: false
@@ -54,8 +52,6 @@ Singleton {
repeat: false repeat: false
onTriggered: { onTriggered: {
detectElogindProcess.running = true; detectElogindProcess.running = true;
detectLoginctlProcess.running = true;
detectSystemctlProcess.running = true;
detectHibernateProcess.running = true; detectHibernateProcess.running = true;
detectPrimeRunProcess.running = true; detectPrimeRunProcess.running = true;
if (!SettingsData.loginctlLockIntegration) { if (!SettingsData.loginctlLockIntegration) {
@@ -91,26 +87,6 @@ Singleton {
} }
} }
Process {
id: detectLoginctlProcess
running: false
command: ["sh", "-c", "command -v loginctl"]
onExited: function (exitCode) {
loginctlCommandAvailable = (exitCode === 0);
}
}
Process {
id: detectSystemctlProcess
running: false
command: ["sh", "-c", "command -v systemctl"]
onExited: function (exitCode) {
systemctlCommandAvailable = (exitCode === 0);
}
}
Process { Process {
id: detectHibernateProcess id: detectHibernateProcess
running: false running: false
@@ -349,14 +325,9 @@ Singleton {
} }
} }
function powerManagerCommand(action) {
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
return [useLoginctl ? "loginctl" : "systemctl", action];
}
function suspend() { function suspend() {
if (SettingsData.customPowerActionSuspend.length === 0) { if (SettingsData.customPowerActionSuspend.length === 0) {
Quickshell.execDetached(powerManagerCommand("suspend")); Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend"]);
} else { } else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]); Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionSuspend]);
} }
@@ -367,13 +338,13 @@ Singleton {
if (SettingsData.customPowerActionHibernate.length > 0) { if (SettingsData.customPowerActionHibernate.length > 0) {
hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate]; hibernateProcess.command = ["sh", "-c", SettingsData.customPowerActionHibernate];
} else { } else {
hibernateProcess.command = powerManagerCommand("hibernate"); hibernateProcess.command = [isElogind ? "loginctl" : "systemctl", "hibernate"];
} }
hibernateProcess.running = true; hibernateProcess.running = true;
} }
function suspendThenHibernate() { function suspendThenHibernate() {
Quickshell.execDetached(powerManagerCommand("suspend-then-hibernate")); Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "suspend-then-hibernate"]);
} }
function suspendWithBehavior(behavior) { function suspendWithBehavior(behavior) {
@@ -388,7 +359,7 @@ Singleton {
function reboot() { function reboot() {
if (SettingsData.customPowerActionReboot.length === 0) { if (SettingsData.customPowerActionReboot.length === 0) {
Quickshell.execDetached(powerManagerCommand("reboot")); Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "reboot"]);
} else { } else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]); Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionReboot]);
} }
@@ -396,7 +367,7 @@ Singleton {
function poweroff() { function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) { if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached(powerManagerCommand("poweroff")); Quickshell.execDetached([isElogind ? "loginctl" : "systemctl", "poweroff"]);
} else { } else {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]); Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionPowerOff]);
} }
+5 -30
View File
@@ -56,19 +56,18 @@ Singleton {
function loadArtwork(url) { function loadArtwork(url) {
if (!url || url === "") { if (!url || url === "") {
// Keep stale art; only blank once the empty url debounce settles. resolvedArtUrl = "";
_lastArtUrl = ""; _lastArtUrl = "";
loading = false; loading = false;
_clearDebounce.restart();
return; return;
} }
_clearDebounce.stop();
if (url === _lastArtUrl) if (url === _lastArtUrl)
return; return;
_lastArtUrl = url; _lastArtUrl = url;
if (url.startsWith("http://") || url.startsWith("https://")) { if (url.startsWith("http://") || url.startsWith("https://")) {
loading = true; loading = true;
resolvedArtUrl = ""; // Clear stale artwork immediately while loading
const targetUrl = url; const targetUrl = url;
const hash = djb2Hash(url); const hash = djb2Hash(url);
const cacheDir = Paths.strip(Paths.imagecache); const cacheDir = Paths.strip(Paths.imagecache);
@@ -135,30 +134,19 @@ Singleton {
} }
loading = true; loading = true;
resolvedArtUrl = ""; // Clear stale artwork immediately while verifying local file
const localUrl = url; const localUrl = url;
const filePath = url.startsWith("file://") ? url.substring(7) : url; const filePath = url.startsWith("file://") ? url.substring(7) : url;
// Cover file often lands after the metadata update, so poll briefly. Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
Proc.runCommand(null, ["sh", "-c", "for i in $(seq 20); do [ -f \"$1\" ] && exit 0; sleep 0.15; done; exit 1", "sh", filePath], (output, exitCode) => {
if (_lastArtUrl !== localUrl) if (_lastArtUrl !== localUrl)
return; return;
resolvedArtUrl = exitCode === 0 ? localUrl : ""; resolvedArtUrl = exitCode === 0 ? localUrl : "";
loading = false; loading = false;
}, 50, 5000); }, 200);
}
Timer {
id: _clearDebounce
interval: 800
onTriggered: {
if (root._lastArtUrl === "")
root.resolvedArtUrl = "";
}
} }
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property string _resolvedTrackKey: ""
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
Connections { Connections {
@@ -169,21 +157,8 @@ Singleton {
function onMetadataChanged() { root._updateArtUrl(); } function onMetadataChanged() { root._updateArtUrl(); }
} }
function _trackKey() {
const p = activePlayer;
if (!p)
return "";
return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || "");
}
function _updateArtUrl() { function _updateArtUrl() {
const url = getArtworkUrl(activePlayer); const url = getArtworkUrl(activePlayer);
const key = _trackKey();
// Ignore metadata jitter once resolved, but only when the url still matches
// (trackArtUrl can update before the title, and skipping then wedges old art).
if (key !== "" && key === _resolvedTrackKey && url === _lastArtUrl && resolvedArtUrl !== "")
return;
_resolvedTrackKey = key;
loadArtwork(url); loadArtwork(url);
} }
} }
-87
View File
@@ -1,87 +0,0 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float phase;
float spin;
float sizePx;
float baseRadiusPx;
float amplitudePx;
float activation;
float energy;
vec2 bandsB;
vec4 bandsA;
vec4 fillColor;
} ubuf;
const float TAU = 6.28318530718;
const float NOISE_FREQ = 1.25;
const float ORBIT_RADIUS = 1.0;
const float WARP_STRENGTH = 0.75;
const float RING_BIAS = 0.25;
const float MID_FREQ = 2.4;
const float MID_GAIN = 0.34;
const float HIGH_FREQ = 5.0;
const float HIGH_GAIN = 0.16;
const float LOBE_MAX = 1.3;
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
return 0.55 * vnoise(p) + 0.3 * vnoise(p * 2.0 + vec2(17.1, 9.2)) + 0.15 * vnoise(p * 3.9 + vec2(4.3, 21.7));
}
void main() {
vec2 p = (qt_TexCoord0 - 0.5) * ubuf.sizePx;
float r = length(p);
float aa = max(fwidth(r), 0.75);
float T = ubuf.phase * TAU;
vec2 dir = r > 0.001 ? p / r : vec2(1.0, 0.0);
float cs = cos(ubuf.spin);
float sn = sin(ubuf.spin);
dir = vec2(dir.x * cs - dir.y * sn, dir.x * sn + dir.y * cs);
vec2 q = dir * NOISE_FREQ;
vec2 orbitA = vec2(cos(T), sin(T)) * ORBIT_RADIUS;
vec2 orbitB = vec2(cos(2.0 * T + 2.1), sin(2.0 * T + 2.1)) * (ORBIT_RADIUS * 0.6);
float w1 = fbm(q + orbitA);
float w2 = fbm(q * 1.3 + orbitB + vec2(3.7, 7.3));
float n = fbm(q + orbitA + WARP_STRENGTH * (vec2(w1, w2) - 0.5));
float mid = max(ubuf.bandsA.z, ubuf.bandsA.w);
float high = max(ubuf.bandsB.x, ubuf.bandsB.y);
vec2 orbitM = vec2(cos(3.0 * T + 0.7), sin(3.0 * T + 0.7)) * 0.9;
vec2 orbitH = vec2(cos(5.0 * T + 4.2), sin(5.0 * T + 4.2)) * 1.2;
float midTerm = (vnoise(dir * MID_FREQ + orbitM) - 0.5) * MID_GAIN * mid;
float highTerm = (vnoise(dir * HIGH_FREQ + orbitH) - 0.5) * HIGH_GAIN * high;
float shaped = smoothstep(0.34, 0.7, n);
float body = ubuf.energy * (RING_BIAS + (1.0 - RING_BIAS) * shaped);
float lobe = clamp(body + midTerm + highTerm, 0.0, LOBE_MAX);
float blobR = ubuf.baseRadiusPx + ubuf.activation * ubuf.amplitudePx * lobe;
float mask = 1.0 - smoothstep(-aa, aa, r - blobR);
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
fragColor = vec4(ubuf.fillColor.rgb * a, a);
}
-50
View File
@@ -1,50 +0,0 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float minH;
float maxH;
vec2 bandsB;
vec4 bandsA;
vec4 fillColor;
} ubuf;
const float BAR_W = 2.0;
const float GAP = 1.5;
const float RADIUS = 1.0;
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + vec2(r);
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
void main() {
vec2 px = vec2(qt_TexCoord0.x * ubuf.widthPx, qt_TexCoord0.y * ubuf.heightPx);
float total = 6.0 * BAR_W + 5.0 * GAP;
float x0 = (ubuf.widthPx - total) * 0.5;
float slot = BAR_W + GAP;
float fi = floor((px.x - x0) / slot);
if (fi < 0.0 || fi > 5.0) {
fragColor = vec4(0.0);
return;
}
int i = int(fi);
float lvl = i == 0 ? ubuf.bandsA.x : i == 1 ? ubuf.bandsA.y : i == 2 ? ubuf.bandsA.z : i == 3 ? ubuf.bandsA.w : i == 4 ? ubuf.bandsB.x : ubuf.bandsB.y;
float h = ubuf.minH + clamp(lvl, 0.0, 1.0) * (ubuf.maxH - ubuf.minH);
float lx = px.x - x0 - fi * slot;
vec2 p = vec2(lx - BAR_W * 0.5, px.y - ubuf.heightPx * 0.5);
float d = sdRoundBar(p, vec2(BAR_W * 0.5, h * 0.5), RADIUS);
float mask = 1.0 - smoothstep(-0.6, 0.6, d);
float a = ubuf.fillColor.a * mask * ubuf.qt_Opacity;
fragColor = vec4(ubuf.fillColor.rgb * a, a);
}
-115
View File
@@ -1,115 +0,0 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float value;
float actualValue;
float phase;
float ampPx;
float wavelengthPx;
float lineWidthPx;
float showActual;
vec4 fillColor;
vec4 trackColor;
vec4 playheadColor;
vec4 actualColor;
} ubuf;
const float TAU = 6.28318530718;
const float AA = 0.75; // pixel-space antialias band
// Signed distance to a rounded box centered at the origin.
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + vec2(r);
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
// Composite a straight-alpha color over a premultiplied accumulator.
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
}
void main() {
float w = ubuf.widthPx;
float h = ubuf.heightPx;
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
float mid = h * 0.5;
float halfW = ubuf.lineWidthPx * 0.5;
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
bool seeking = ubuf.showActual > 0.5;
float loX = min(playX, actualX);
float hiX = max(playX, actualX);
float fillEnd = seeking ? loX : playX; // filled progress ends here
float actStart = seeking ? loX : playX; // seek-preview segment
float actEnd = seeking ? hiX : playX;
float trackStart = seeking ? hiX : playX; // unplayed remainder
// Perpendicular distance to the animated sine stroke.
float ang = k * px.x + ubuf.phase;
float wy = mid + ubuf.ampPx * sin(ang);
float slope = ubuf.ampPx * k * cos(ang);
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
float aaW = AA;
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
// Straight remainder line.
float dLine = abs(px.y - mid);
float aaL = AA;
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
vec4 col = vec4(0.0);
// 1. Track (unplayed remainder), to the right of the progress head.
{
float m = lineStroke * step(trackStart, px.x);
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
}
// 2. Seek-preview segment (only while seeking).
if (seeking) {
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 3. Filled progress wave.
{
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
// Rounded start cap.
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
m = max(m, capM * step(halfW - 1.0, px.x));
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
}
// 4. Actual-position marker (only while seeking).
if (seeking) {
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 5. Playhead pill (on top).
{
float phW = 3.5;
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
}
fragColor = col * ubuf.qt_Opacity;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1
View File
@@ -85,7 +85,6 @@ Item {
anchors.fill: parent anchors.fill: parent
imagePath: root.imagePath imagePath: root.imagePath
maxCacheSize: root.iconSize * 2 maxCacheSize: root.iconSize * 2
animate: false
visible: root.isImage && status === Image.Ready visible: root.isImage && status === Image.Ready
} }
+108 -124
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -14,36 +15,6 @@ Item {
property bool showAnimation: true property bool showAnimation: true
property real animationScale: 1.0 property real animationScale: 1.0
readonly property real blobBaseRadiusFactor: 0.43
readonly property real blobAmplitudeFactor: 0.115
readonly property real blobOvershoot: 1.15
readonly property real blobEnergySensitivity: 1.15
readonly property real cavaFullScale: 45
readonly property real blobAttack: 0.75
readonly property real blobRelease: 0.2
readonly property real blobBeatBoost: 2.5
readonly property real blobBeatKick: 4
readonly property real blobOnsetThreshold: 1.4
readonly property real blobSpringStiffness: 220
readonly property real blobSpringDamping: 19
readonly property real blobMorphSpeed: 0.05
readonly property real blobMorphBoost: 1.7
readonly property real blobSpinSpeed: 0.03
readonly property bool blobActive: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation && albumArtStatus === Image.Ready
property var smoothedBands: [0, 0, 0, 0, 0, 0]
property var slowBands: [0, 0, 0, 0, 0, 0]
property var bandTargets: [0, 0, 0, 0, 0, 0]
property var bandDisplay: [0, 0, 0, 0, 0, 0]
property var prevLevels: [0, 0, 0, 0, 0, 0]
readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35]
property real fluxAvg: 0.02
property real loudCtx: 0.1
property int beatCooldown: 0
property real energyTarget: 0
property real energyPos: 0
property real energyVel: 0
onActivePlayerChanged: { onActivePlayerChanged: {
lastValidArtUrl = ""; lastValidArtUrl = "";
} }
@@ -54,62 +25,6 @@ Item {
} }
} }
function updateBands() {
const vals = CavaService.values;
if (!vals || vals.length < 6)
return;
const s = smoothedBands;
const slow = slowBands;
const out = bandTargets;
const prev = prevLevels;
const w = fluxWeights;
let flux = 0;
for (let i = 0; i < 6; i++) {
const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale;
flux += Math.max(0, level - prev[i]) * w[i];
prev[i] = level;
const alpha = level > s[i] ? blobAttack : blobRelease;
s[i] += alpha * (level - s[i]);
slow[i] += 0.05 * (level - slow[i]);
const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost;
out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity);
}
const ratio = flux / Math.max(fluxAvg, 0.004);
fluxAvg += 0.06 * (flux - fluxAvg);
if (beatCooldown > 0) {
beatCooldown--;
} else if (ratio > blobOnsetThreshold && flux > 0.008) {
energyVel += blobBeatKick * Math.min(2.5, ratio - 1);
beatCooldown = 3;
}
const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]);
loudCtx += 0.03 * (loud - loudCtx);
const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1);
energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge));
}
function stepBlob(dt) {
energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt;
energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt));
blobEffect.energy = energyPos;
const d = bandDisplay;
const t = bandTargets;
const f = Math.min(1, dt * 14);
for (let i = 0; i < 6; i++) {
d[i] += f * (t[i] - d[i]);
}
blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]);
blobEffect.bandsB = Qt.vector2d(d[4], d[5]);
const speed = 1 + energyPos * blobMorphBoost;
blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1;
blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718;
}
Loader { Loader {
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
sourceComponent: Component { sourceComponent: Component {
@@ -119,50 +34,119 @@ Item {
} }
} }
Connections { Shape {
target: CavaService id: morphingBlob
enabled: blobEffect.visible width: parent.width * 1.1
function onValuesChanged() { height: parent.height * 1.1
root.updateBands();
}
}
FrameAnimation {
running: blobEffect.visible
onTriggered: root.stepBlob(Math.min(frameTime, 0.05))
}
ShaderEffect {
id: blobEffect
readonly property real span: Math.min(root.width, root.height)
width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4
height: width
anchors.centerIn: parent anchors.centerIn: parent
visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
z: 0 z: 0
visible: root.blobActive || activation > 0.004 layer.enabled: false
property real phase: 0 readonly property real centerX: width / 2
property real spin: 0 readonly property real centerY: height / 2
property real sizePx: width readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale readonly property int segments: 28
property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale
property real activation: root.blobActive ? 1 : 0
property real energy: 0
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
property vector2d bandsB: Qt.vector2d(0, 0)
readonly property color accentColor: MediaAccentService.accent
property vector4d fillColor: Qt.vector4d(accentColor.r, accentColor.g, accentColor.b, accentColor.a)
Behavior on activation { property var audioLevels: {
NumberAnimation { if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
duration: 550 return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6];
easing.type: Easing.InOutQuad }
return CavaService.values;
}
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
property var cubics: []
Connections {
target: CavaService
function onValuesChanged() {
if (morphingBlob.visible) {
morphingBlob.updatePath();
}
} }
} }
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/blob.frag.qsb") Component {
id: cubicSegment
PathCubic {}
}
Component {
id: pathMoveComp
PathMove {}
}
Component.onCompleted: {
shapePath.pathElements.push(pathMoveComp.createObject(shapePath));
for (let i = 0; i < segments; i++) {
const seg = cubicSegment.createObject(shapePath);
shapePath.pathElements.push(seg);
cubics.push(seg);
}
updatePath();
}
function updatePath() {
if (cubics.length === 0)
return;
const alpha = 0.35;
const minLen = Math.min(smoothedLevels.length, audioLevels.length);
for (let i = 0; i < minLen; i++) {
smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]);
}
const angleStep = 2 * Math.PI / segments;
const tension3 = 0.16666667;
const startMove = shapePath.pathElements[0];
const points = new Array(segments);
for (let i = 0; i < segments; i++) {
const angle = i * angleStep;
const audioIndex = i % 10;
const rawLevel = smoothedLevels[audioIndex] || 0;
const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
const radius = baseRadius * (1.0 + audioLevel);
points[i] = {
x: centerX + Math.cos(angle) * radius,
y: centerY + Math.sin(angle) * radius
};
}
startMove.x = points[0].x;
startMove.y = points[0].y;
for (let i = 0; i < segments; i++) {
const p0 = points[(i + segments - 1) % segments];
const p1 = points[i];
const p2 = points[(i + 1) % segments];
const p3 = points[(i + 2) % segments];
const seg = cubics[i];
seg.control1X = p1.x + (p2.x - p0.x) * tension3;
seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
seg.control2X = p2.x - (p3.x - p1.x) * tension3;
seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
seg.x = p2.x;
seg.y = p2.y;
}
}
ShapePath {
id: shapePath
fillColor: Theme.primary
strokeColor: "transparent"
strokeWidth: 0
joinStyle: ShapePath.RoundJoin
fillRule: ShapePath.WindingFill
}
} }
DankCircularImage { DankCircularImage {
@@ -174,7 +158,7 @@ Item {
imageSource: artUrl || lastValidArtUrl || "" imageSource: artUrl || lastValidArtUrl || ""
fallbackIcon: "album" fallbackIcon: "album"
border.color: MediaAccentService.accent border.color: Theme.primary
border.width: 2 border.width: 2
onImageSourceChanged: { onImageSourceChanged: {
+85 -44
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
import Quickshell.Widgets import QtQuick.Effects
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
@@ -13,17 +13,9 @@ Rectangle {
property bool cacheImages: true property bool cacheImages: true
property bool hasImage: imageSource !== "" property bool hasImage: imageSource !== ""
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://") readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
// Probe with AnimatedImage first; once loaded, check frameCount to decide.
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1 readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error readonly property var activeImage: isAnimated ? probe : staticImage
readonly property var activeImage: {
if (isAnimated)
return probe;
if (staticImage.status === Image.Ready)
return staticImage;
if (probe.status === Image.Ready && probe.source !== "")
return probe;
return staticImage;
}
property int imageStatus: activeImage.status property int imageStatus: activeImage.status
signal imageSaved(string filePath) signal imageSaved(string filePath)
@@ -65,47 +57,96 @@ Rectangle {
border.color: "transparent" border.color: "transparent"
border.width: 0 border.width: 0
ClippingRectangle { // Probe: loads as AnimatedImage to detect frame count.
AnimatedImage {
id: probe
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 anchors.margins: 2
radius: Math.min(width, height) / 2 asynchronous: true
color: "transparent" fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: false
source: root.shouldProbe ? root.imageSource : ""
}
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready. // Static fallback: used once probe confirms the image is not animated.
AnimatedImage { Image {
id: probe id: staticImage
anchors.fill: parent anchors.fill: parent
asynchronous: true anchors.margins: 2
fillMode: Image.PreserveAspectCrop asynchronous: true
smooth: true fillMode: Image.PreserveAspectCrop
mipmap: true smooth: true
cache: root.cacheImages mipmap: true
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== "" cache: root.cacheImages
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : "" visible: false
} sourceSize.width: Math.max(width * 2, 128)
sourceSize.height: Math.max(height * 2, 128)
source: !root.shouldProbe ? root.imageSource : ""
}
// Takes over once the probe settles on a non-animated image, then latches. // Once the probe loads, if not animated, hand off to Image and unload probe.
Image { Connections {
id: staticImage target: probe
anchors.fill: parent function onStatusChanged() {
asynchronous: true if (!root.shouldProbe)
fillMode: Image.PreserveAspectCrop return;
smooth: true switch (probe.status) {
mipmap: true case Image.Ready:
cache: root.cacheImages if (probe.frameCount <= 1) {
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== "" staticImage.source = root.imageSource;
sourceSize.width: Math.max(width * 2, 128) probe.source = "";
sourceSize.height: Math.max(height * 2, 128) }
source: { break;
if (!root.shouldProbe) case Image.Error:
return root.imageSource; staticImage.source = root.imageSource;
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null) probe.source = "";
return root.imageSource; break;
return "";
} }
} }
} }
// If imageSource changes, reset: re-probe with AnimatedImage.
onImageSourceChanged: {
if (root.shouldProbe) {
staticImage.source = "";
probe.source = root.imageSource;
} else {
probe.source = "";
staticImage.source = root.imageSource;
}
}
MultiEffect {
anchors.fill: parent
anchors.margins: 2
source: root.activeImage
maskEnabled: true
maskSource: circularMask
visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
Item {
id: circularMask
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
layer.enabled: true
layer.smooth: true
visible: false
Rectangle {
anchors.fill: parent
radius: width / 2
color: "black"
antialiasing: true
}
}
AppIconRenderer { AppIconRenderer {
anchors.centerIn: parent anchors.centerIn: parent
width: Math.round(parent.width * 0.75) width: Math.round(parent.width * 0.75)
+12 -3
View File
@@ -1249,14 +1249,14 @@ Item {
// Fast fade duration for superseded close. // Fast fade duration for superseded close.
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)) readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
readonly property real publishedOpacity: opacity property real publishedOpacity: _targetOpacity
opacity: _targetOpacity opacity: _targetOpacity
visible: _renderActive visible: _renderActive
scale: contentContainer.scaleValue scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX, root.dpr) x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY, root.dpr) y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1) layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
layer.smooth: false layer.smooth: false
@@ -1276,6 +1276,15 @@ Item {
} }
} }
Behavior on publishedOpacity {
enabled: contentWrapper._fadeWithOpacity
NumberAnimation {
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Connections { Connections {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
+8 -34
View File
@@ -220,31 +220,6 @@ Item {
onTriggered: root._bgCommitWindow = false onTriggered: root._bgCommitWindow = false
} }
// An idle layer surface won't commit the cleared blur region on auto-close, so the
// blur sticks; pulse updatesEnabled false->true to force a re-commit.
property bool _blurCommitSuppress: false
Timer {
id: blurCommitPulseTimer
interval: 16
onTriggered: root._blurCommitSuppress = false
}
function _pulseBlurCommit() {
if (!backgroundWindow.visible)
return;
_blurCommitSuppress = true;
blurCommitPulseTimer.restart();
}
Connections {
target: overlayLoader.item
ignoreUnknownSignals: true
function onOverlayBlurActiveChanged() {
root._pulseBlurCommit();
}
}
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) { function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
const newX = Theme.snap(bodyX, dpr); const newX = Theme.snap(bodyX, dpr);
const newY = Theme.snap(bodyY, dpr); const newY = Theme.snap(bodyY, dpr);
@@ -565,7 +540,7 @@ Item {
// Skip buffer updates when there's nothing to render. Briefly flipped // Skip buffer updates when there's nothing to render. Briefly flipped
// true via _bgCommitWindow when _surfaceBodyW/H changes so the // true via _bgCommitWindow when _surfaceBodyW/H changes so the
// contentHoleRect mask carve-out actually commits to the compositor. // contentHoleRect mask carve-out actually commits to the compositor.
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow) updatesEnabled: root.overlayContent !== null || root._bgCommitWindow
WlrLayershell.namespace: root.layerNamespace + ":background" WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: root.effectivePopoutLayer WlrLayershell.layer: root.effectivePopoutLayer
@@ -645,15 +620,13 @@ Item {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6))
readonly property real visibleScale: s * op
readonly property bool revealClipActive: root.fluidStandaloneActive readonly property bool revealClipActive: root.fluidStandaloneActive
// Blur tracks the surface's scaled rect, matching the connected backend // Blur tracks the surface's scaled rect, matching the connected backend
blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - visibleScale) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * visibleScale) : 0 blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * s) : 0
blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * visibleScale) : 0 blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * s) : 0
blurRadius: Theme.cornerRadius blurRadius: Theme.cornerRadius
clipEnabled: revealClipActive clipEnabled: revealClipActive
clipX: contentContainer.x + contentContainer.revealX clipX: contentContainer.x + contentContainer.revealX
@@ -769,7 +742,8 @@ Item {
QtObject { QtObject {
id: morph id: morph
property real openProgress: 0 property real openProgress: 0
onOpenProgressChanged: root._kickBlurCommit() onOpenProgressChanged: if (root.fluidStandaloneActive)
root._kickBlurCommit()
Behavior on openProgress { Behavior on openProgress {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
@@ -872,7 +846,7 @@ Item {
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
layer.enabled: !Theme.isDirectionalEffect && _renderActive layer.enabled: !Theme.isDirectionalEffect && publishedOpacity < 1
layer.smooth: false layer.smooth: false
layer.textureSize: Qt.size(0, 0) layer.textureSize: Qt.size(0, 0)
+3 -6
View File
@@ -142,10 +142,7 @@ Item {
value: root.value value: root.value
actualValue: root.playerValue actualValue: root.playerValue
showActualPlaybackState: root.isSeeking showActualPlaybackState: root.isSeeking
fillColor: MediaAccentService.accent actualProgressColor: Theme.onSurface_38
playheadColor: MediaAccentService.accent
trackColor: MediaAccentService.accentTrack
actualProgressColor: MediaAccentService.accentSubtle
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
MouseArea { MouseArea {
@@ -182,8 +179,8 @@ Item {
Item { Item {
property real lineWidth: 3 property real lineWidth: 3
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40) property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
property color fillColor: MediaAccentService.accent property color fillColor: Theme.primary
property color playheadColor: MediaAccentService.accent property color playheadColor: Theme.primary
property color actualProgressColor: Theme.onSurface_38 property color actualProgressColor: Theme.onSurface_38
readonly property real midY: height / 2 readonly property real midY: height / 2
-5
View File
@@ -256,11 +256,6 @@ FocusScope {
} }
} }
function snapIndicator() {
indicator.initialSetupComplete = false;
updateIndicator();
}
onCurrentIndexChanged: { onCurrentIndexChanged: {
Qt.callLater(updateIndicator); Qt.callLater(updateIndicator);
} }
+234 -20
View File
@@ -1,8 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Shapes
import qs.Common import qs.Common
// Wave progress indicator: track, animated fill, seek preview and playhead are
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
Item { Item {
id: root id: root
@@ -20,6 +19,19 @@ Item {
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Theme.onSurface_38 property color actualProgressColor: Theme.onSurface_38
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) {
return Math.round(v * dpr) / dpr;
}
readonly property real playX: snap(root.width * root.value)
readonly property real actualX: snap(root.width * root.actualValue)
readonly property real midY: snap(height / 2)
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
Behavior on currentAmp { Behavior on currentAmp {
NumberAnimation { NumberAnimation {
duration: 300 duration: 300
@@ -27,34 +39,236 @@ Item {
} }
} }
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0 onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
Component.onCompleted: currentAmp = isPlaying ? amp : 0
ShaderEffect { Shape {
id: flatTrack
anchors.fill: parent anchors.fill: parent
blending: true antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
readonly property real widthPx: width ShapePath {
readonly property real heightPx: height strokeColor: root.trackColor
readonly property real value: root.value strokeWidth: snap(root.lineWidth)
readonly property real actualValue: root.actualValue capStyle: ShapePath.RoundCap
readonly property real phase: root.phase joinStyle: ShapePath.RoundJoin
readonly property real ampPx: root.currentAmp fillColor: "transparent"
readonly property real wavelengthPx: root.wavelength PathMove {
readonly property real lineWidthPx: root.lineWidth id: flatStart
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0 x: Math.min(root.width, snap(root.playX + playhead.width / 2))
readonly property color fillColor: root.fillColor y: root.midY
readonly property color trackColor: root.trackColor }
readonly property color playheadColor: root.playheadColor PathLine {
readonly property color actualColor: root.actualProgressColor id: flatEnd
x: root.width
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb") y: root.midY
}
}
} }
Item {
id: waveClip
anchors.fill: parent
clip: true
readonly property real startX: snap(root.lineWidth / 2)
readonly property real aaBias: (0.25 / root.dpr)
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
Rectangle {
id: mask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: 0
width: waveClip.endX
color: "transparent"
clip: true
Shape {
id: waveShape
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
id: wavePath
strokeColor: root.fillColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
id: waveSvg
path: ""
}
}
}
}
Rectangle {
id: actualMask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: waveClip.gapStartX
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
color: "transparent"
clip: true
visible: (root.previewBehind || root.previewAhead) && width > 0
Shape {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
strokeColor: root.actualProgressColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: waveSvg.path
}
}
}
}
Rectangle {
id: startCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.fillColor
x: waveClip.startX - width / 2
y: waveY(waveClip.startX) - height / 2
visible: waveClip.endX > waveClip.startX
z: 2
}
Rectangle {
id: endCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.fillColor
x: waveClip.endX - width / 2
y: waveY(waveClip.endX) - height / 2
visible: waveClip.endX > waveClip.startX
z: 2
}
Rectangle {
id: actualEndCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.actualProgressColor
x: waveClip.gapEndX - width / 2
y: waveY(waveClip.gapEndX) - height / 2
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
z: 2
}
Rectangle {
id: actualMarker
width: 2
height: Math.max(root.lineWidth + 4, 10)
radius: width / 2
color: root.actualProgressColor
x: root.actualX - width / 2
y: root.midY - height / 2
visible: root.showActualPlaybackState
z: 2
}
}
Rectangle {
id: playhead
width: 3.5
height: Math.max(root.lineWidth + 12, 16)
radius: width / 2
color: root.playheadColor
x: root.playX - width / 2
y: root.midY - height / 2
z: 3
}
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
function wrapMod(a, m) {
let r = a % m;
return r < 0 ? r + m : r;
}
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
}
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
FrameAnimation { FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) running: root.visible && (root.isPlaying || root.currentAmp > 0)
onTriggered: { onTriggered: {
if (root.isPlaying) if (root.isPlaying)
root.phase += 0.03 * frameTime * 60; root.phase += 0.03 * frameTime * 60;
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
} }
} }
function buildStaticWave() {
const start = waveClip.startX - 2 * root.wavelength;
const end = width + 2 * root.wavelength;
if (end <= start) {
waveSvg.path = "";
return;
}
const kLocal = k;
const halfPeriod = root.wavelength / 2;
function y0(x) {
return root.midY + root.currentAmp * Math.sin(kLocal * x);
}
function dy0(x) {
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
}
let x0 = start;
let d = `M ${x0} ${y0(x0)}`;
while (x0 < end) {
const x1 = Math.min(x0 + halfPeriod, end);
const dx = x1 - x0;
const yA = y0(x0), yB = y0(x1);
const dyA = dy0(x0), dyB = dy0(x1);
const c1x = x0 + dx / 3;
const c1y = yA + (dyA * dx) / 3;
const c2x = x1 - dx / 3;
const c2y = yB - (dyB * dx) / 3;
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
x0 = x1;
}
waveSvg.path = d;
}
Component.onCompleted: {
currentAmp = isPlaying ? amp : 0;
buildStaticWave();
}
onWidthChanged: {
flatEnd.x = width;
buildStaticWave();
}
onHeightChanged: buildStaticWave()
onCurrentAmpChanged: buildStaticWave()
onWavelengthChanged: {
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
buildStaticWave();
}
} }
+1 -2
View File
@@ -31,8 +31,7 @@ LANGUAGES = {
"de": "de.json", "de": "de.json",
"sv": "sv.json", "sv": "sv.json",
"vi": "vi.json", "vi": "vi.json",
"eo": "eo.json", "eo": "eo.json"
"ko": "ko.json"
} }
def error(msg): def error(msg):