1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-08 05:22:06 -04:00

Compare commits

..

2 Commits

Author SHA1 Message Date
bbedward
6e6416c8ba blur: fix dankbar auto-hide blur, fix synchronization in popouts and
modals
2026-03-30 10:40:52 -04:00
bbedward
a0b2debd7e blur: add blur support with ext-bg-effect 2026-03-30 09:33:26 -04:00
80 changed files with 24294 additions and 22469 deletions

View File

@@ -1,40 +0,0 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
"github.com/spf13/cobra"
)
var blurCmd = &cobra.Command{
Use: "blur",
Short: "Background blur utilities",
}
var blurCheckCmd = &cobra.Command{
Use: "check",
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
Args: cobra.NoArgs,
Run: runBlurCheck,
}
func init() {
blurCmd.AddCommand(blurCheckCmd)
}
func runBlurCheck(cmd *cobra.Command, args []string) {
supported, err := blur.ProbeSupport()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
switch supported {
case true:
fmt.Println("supported")
default:
fmt.Println("unsupported")
}
}

View File

@@ -525,6 +525,5 @@ func getCommonCommands() []*cobra.Command {
configCmd,
dlCmd,
randrCmd,
blurCmd,
}
}

View File

@@ -820,14 +820,10 @@ func checkOptionalDependencies() []checkResult {
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
terminals = slices.DeleteFunc(terminals, func(t string) bool {
return !utils.CommandExists(t)
})
if len(terminals) > 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
} else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
}
networkResult, err := network.DetectNetworkStack()

View File

@@ -109,41 +109,16 @@ func updateArchLinux() error {
}
var packageName string
var isAUR bool
if isArchPackageInstalled("dms-shell") {
packageName = "dms-shell"
if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
} else if isArchPackageInstalled("dms-shell-git") {
packageName = "dms-shell-git"
isAUR = true
} else if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
isAUR = true
} else {
fmt.Println("Info: No dms-shell package found.")
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
fmt.Println("Info: Falling back to git-based update method...")
return updateOtherDistros()
}
if !isAUR {
fmt.Printf("This will update %s using pacman.\n", packageName)
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
return err
}
fmt.Println("dms successfully updated")
return nil
}
var helper string
var updateCmd *exec.Cmd

View File

@@ -5,7 +5,6 @@ package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
@@ -31,9 +30,7 @@ func init() {
}
func main() {
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

@@ -5,7 +5,6 @@ package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
@@ -28,9 +27,7 @@ func init() {
}
func main() {
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

@@ -7,22 +7,6 @@ import (
"strings"
)
// isReadOnlyCommand returns true if the CLI args indicate a command that is
// safe to run as root (e.g. shell completion, help).
func isReadOnlyCommand(args []string) bool {
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
continue
}
switch arg {
case "completion", "help", "__complete":
return true
}
return false
}
return false
}
func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run()

View File

@@ -1,35 +0,0 @@
package blur
import (
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
func ProbeSupport() (bool, error) {
display, err := client.Connect("")
if err != nil {
return false, err
}
defer display.Context().Close()
registry, err := display.GetRegistry()
if err != nil {
return false, err
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case extBackgroundEffectInterface:
found = true
}
})
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
return false, err
}
return found, nil
}

View File

@@ -1,6 +1,7 @@
package clipboard
import (
"bytes"
"fmt"
"io"
"os"
@@ -12,142 +13,66 @@ import (
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const envServe = "_DMS_CLIPBOARD_SERVE"
const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() {
if os.Getenv(envServe) == "" {
return
}
mimeType := os.Getenv(envMime)
pasteOnce := os.Getenv(envPasteOnce) == "1"
cachePath := os.Getenv(envCacheFile)
var data []byte
var err error
switch {
case cachePath != "":
data, err = os.ReadFile(cachePath)
os.Remove(cachePath)
default:
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
os.Exit(1)
}
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func Copy(data []byte, mimeType string) error {
return copyForkCached(data, mimeType, false)
return CopyReader(bytes.NewReader(data), mimeType, false, false)
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return serveClipboard(data, mimeType, pasteOnce)
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return copyForkCached(data, mimeType, pasteOnce)
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if foreground {
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
if !foreground {
return copyFork(data, mimeType, pasteOnce)
}
return copyFork(data, mimeType, pasteOnce)
return copyServeReader(data, mimeType, pasteOnce)
}
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
cmd := exec.Command(os.Args[0])
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce {
args = append(args, "--paste-once")
}
args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Env = append(os.Environ(),
envServe+"=1",
envMime+"="+mimeType,
)
if pasteOnce {
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
}
cmd.Env = append(cmd.Env, extra...)
return cmd
}
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
func waitReady(cmd *exec.Cmd) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
cacheFile, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create cache file: %w", err)
}
cachePath := cacheFile.Name()
if _, err := cacheFile.Write(data); err != nil {
cacheFile.Close()
os.Remove(cachePath)
return fmt.Errorf("write cache file: %w", err)
}
if err := cacheFile.Close(); err != nil {
os.Remove(cachePath)
return fmt.Errorf("close cache file: %w", err)
}
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
cmd.Stdin = nil
if err := waitReady(cmd); err != nil {
os.Remove(cachePath)
return err
}
return nil
}
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := newForkCmd(mimeType, pasteOnce)
switch src := data.(type) {
case *os.File:
cmd.Stdin = src
return waitReady(cmd)
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
default:
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
@@ -158,22 +83,50 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func signalReady() {
if os.Getenv(envServe) == "" {
if os.Getenv("DMS_CLIP_FORKED") == "" {
return
}
os.Stdout.Write([]byte{1})
}
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
@@ -194,7 +147,7 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*")
}
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
@@ -236,10 +189,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
@@ -278,12 +233,18 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
_, _ = file.Write(data)
if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
select {
case pasted <- struct{}{}:
default:
@@ -305,6 +266,8 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
select {
case <-cancelled:
return nil
case err := <-sendErr:
return err
case <-pasted:
if pasteOnce {
return nil
@@ -558,10 +521,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr)
}
if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
}
defer dataControlMgr.Destroy()
if seat == nil {
return fmt.Errorf("no seat available")
}
@@ -589,12 +554,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false)
defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerMap[e.MimeType]; ok {
_, _ = file.Write(data)
file.Write(data)
}
select {

View File

@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
# === Sizing & Layout ===
bind = SUPER, R, layoutmsg, togglesplit
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
bind = SUPER CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = SUPER, mouse:272, Move window, movewindow

View File

@@ -94,7 +94,6 @@ windowrule = tile on, match:class ^(gnome-control-center)$
windowrule = tile on, match:class ^(pavucontrol)$
windowrule = tile on, match:class ^(nm-connection-editor)$
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
windowrule = float on, match:class ^(gnome-calculator)$
windowrule = float on, match:class ^(galculator)$
windowrule = float on, match:class ^(blueman-manager)$

View File

@@ -224,7 +224,6 @@ window-rule {
open-floating false
}
window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"#

View File

@@ -242,7 +242,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
if a.packageInstalled("dms-shell-bin") {
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -536,7 +540,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
var dmsShell []string
for _, pkg := range packages {
if pkg == "dms-shell-git" {
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
dmsShell = append(dmsShell, pkg)
} else {
isDep := false
@@ -617,7 +621,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
}
}
if pkg == "dms-shell-git" {
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{
"depends = quickshell",
@@ -640,7 +644,15 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
}
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
{
if pkg == "dms-shell-bin" {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -727,9 +739,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
CommandInfo: "sudo pacman -U built-package",
}
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
var files []string
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
// For DMS split packages, install base package
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
matches, err := filepath.Glob(pattern)
if err == nil {
for _, match := range matches {
basename := filepath.Base(match)
// Always include base package
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
files = append(files, match)
}
}
}
// Also update compositor-specific packages if they're installed
if strings.HasSuffix(pkg, "-git") {
if a.packageInstalled("dms-shell-hyprland-git") {
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
files = append(files, hyprlandMatches[0])
}
}
if a.packageInstalled("dms-shell-niri-git") {
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
files = append(files, niriMatches[0])
}
}
}
} else {
// For other packages, install all built packages
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
}
if len(files) == 0 {
return fmt.Errorf("no package files found after building %s", pkg)

View File

@@ -444,21 +444,20 @@ func GetFocusedMonitor() string {
type outputInfo struct {
x, y int32
scale float64
transform int32
}
func getAllOutputInfos() map[string]*outputInfo {
func getOutputInfo(outputName string) (*outputInfo, bool) {
display, err := client.Connect("")
if err != nil {
return nil
return nil, false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil
return nil, false
}
var outputManager *wlr_output_management.ZwlrOutputManagerV1
@@ -477,17 +476,16 @@ func getAllOutputInfos() map[string]*outputInfo {
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil
return nil, false
}
if outputManager == nil {
return nil
return nil, false
}
type headState struct {
name string
x, y int32
scale float64
transform int32
}
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
@@ -503,9 +501,6 @@ func getAllOutputInfos() map[string]*outputInfo {
state.x = pe.X
state.y = pe.Y
})
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
state.scale = se.Scale
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform
})
@@ -516,32 +511,21 @@ func getAllOutputInfos() map[string]*outputInfo {
for !done {
if err := ctx.Dispatch(); err != nil {
return nil
return nil, false
}
}
result := make(map[string]*outputInfo, len(heads))
for _, state := range heads {
if state.name == "" {
continue
}
result[state.name] = &outputInfo{
x: state.x,
y: state.y,
scale: state.scale,
transform: state.transform,
if state.name == outputName {
return &outputInfo{
x: state.x,
y: state.y,
transform: state.transform,
}, true
}
}
return result
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
infos := getAllOutputInfos()
if infos == nil {
return nil, false
}
info, ok := infos[outputName]
return info, ok
return nil, false
}
func getDWLActiveWindow() (*WindowGeometry, error) {

View File

@@ -2,7 +2,6 @@ package screenshot
import (
"fmt"
"math"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -305,20 +304,22 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs available")
}
if len(outputs) == 1 {
return s.captureWholeOutput(outputs[0])
}
wlrInfos := getAllOutputInfos()
type pendingOutput struct {
// Capture all outputs first to get actual buffer sizes
type capturedOutput struct {
output *WaylandOutput
result *CaptureResult
logX float64
logY float64
scale float64
physX int
physY int
}
var pending []pendingOutput
maxScale := 1.0
captured := make([]capturedOutput, 0, len(outputs))
var minX, minY, maxX, maxY int
first := true
for _, output := range outputs {
result, err := s.captureWholeOutput(output)
@@ -327,74 +328,50 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
continue
}
logX, logY := float64(output.x), float64(output.y)
outX, outY := output.x, output.y
scale := float64(output.scale)
switch DetectCompositor() {
case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
logX, logY = float64(hx), float64(hy)
outX, outY = hx, hy
}
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
scale = hs
if s := GetHyprlandMonitorScale(output.name); s > 0 {
scale = s
}
default:
if wlrInfos != nil {
if info, ok := wlrInfos[output.name]; ok {
logX, logY = float64(info.x), float64(info.y)
if info.scale > 0 {
scale = info.scale
}
}
case CompositorDWL:
if info, ok := getOutputInfo(output.name); ok {
outX, outY = info.x, info.y
}
}
if scale <= 0 {
scale = 1.0
}
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
if scale > maxScale {
maxScale = scale
}
}
physX := int(float64(outX) * scale)
physY := int(float64(outY) * scale)
if len(pending) == 0 {
return nil, fmt.Errorf("failed to capture any outputs")
}
if len(pending) == 1 {
return pending[0].result, nil
}
captured = append(captured, capturedOutput{
output: output,
result: result,
physX: physX,
physY: physY,
})
type layoutEntry struct {
result *CaptureResult
canvasX int
canvasY int
canvasW int
canvasH int
}
entries := make([]layoutEntry, len(pending))
var minX, minY, maxX, maxY int
right := physX + result.Buffer.Width
bottom := physY + result.Buffer.Height
for i, p := range pending {
cx := int(math.Round(p.logX * maxScale))
cy := int(math.Round(p.logY * maxScale))
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
right := cx + cw
bottom := cy + ch
if i == 0 {
minX, minY, maxX, maxY = cx, cy, right, bottom
if first {
minX, minY = physX, physY
maxX, maxY = right, bottom
first = false
continue
}
if cx < minX {
minX = cx
if physX < minX {
minX = physX
}
if cy < minY {
minY = cy
if physY < minY {
minY = physY
}
if right > maxX {
maxX = right
@@ -404,26 +381,35 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
}
}
if len(captured) == 0 {
return nil, fmt.Errorf("failed to capture any outputs")
}
if len(captured) == 1 {
return captured[0].result, nil
}
totalW := maxX - minX
totalH := maxY - minY
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
compositeStride := totalW * 4
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
if err != nil {
for _, e := range entries {
e.result.Buffer.Close()
for _, c := range captured {
c.result.Buffer.Close()
}
return nil, fmt.Errorf("create composite buffer: %w", err)
}
composite.Clear()
var format uint32
for _, e := range entries {
for _, c := range captured {
if format == 0 {
format = e.result.Format
format = c.result.Format
}
s.blitBufferScaled(composite, e.result.Buffer,
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
e.result.YInverted)
e.result.Buffer.Close()
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
c.result.Buffer.Close()
}
return &CaptureResult{
@@ -433,44 +419,32 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
}, nil
}
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
if dstW <= 0 || dstH <= 0 {
return
}
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
srcData := src.Data()
dstData := dst.Data()
for dy := 0; dy < dstH; dy++ {
canvasY := dstY + dy
if canvasY < 0 || canvasY >= dst.Height {
continue
}
srcY := dy * src.Height / dstH
for srcY := 0; srcY < src.Height; srcY++ {
actualSrcY := srcY
if yInverted {
srcY = src.Height - 1 - srcY
actualSrcY = src.Height - 1 - srcY
}
if srcY < 0 || srcY >= src.Height {
dy := dstY + srcY
if dy < 0 || dy >= dst.Height {
continue
}
srcRowOff := srcY * src.Stride
dstRowOff := canvasY * dst.Stride
srcRowOff := actualSrcY * src.Stride
dstRowOff := dy * dst.Stride
for dx := 0; dx < dstW; dx++ {
canvasX := dstX + dx
if canvasX < 0 || canvasX >= dst.Width {
continue
}
srcX := dx * src.Width / dstW
if srcX >= src.Width {
for srcX := 0; srcX < src.Width; srcX++ {
dx := dstX + srcX
if dx < 0 || dx >= dst.Width {
continue
}
si := srcRowOff + srcX*4
di := dstRowOff + canvasX*4
di := dstRowOff + dx*4
if si+3 >= len(srcData) || di+3 >= len(dstData) {
continue

View File

@@ -31,7 +31,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -73,7 +72,6 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
var trayRecoveryManager *trayrecovery.Manager
var locationManager *location.Manager
var geoClientInstance geolocation.Client
@@ -396,18 +394,6 @@ func InitializeThemeModeManager() error {
return nil
}
func InitializeTrayRecoveryManager() error {
manager, err := trayrecovery.NewManager()
if err != nil {
return err
}
trayRecoveryManager = manager
log.Info("TrayRecovery manager initialized")
return nil
}
func InitializeLocationManager(geoClient geolocation.Client) error {
manager, err := location.NewManager(geoClient)
if err != nil {
@@ -1339,9 +1325,6 @@ func cleanupManagers() {
if themeModeManager != nil {
themeModeManager.Close()
}
if trayRecoveryManager != nil {
trayRecoveryManager.Close()
}
if wlContext != nil {
wlContext.Close()
}
@@ -1627,18 +1610,6 @@ func Start(printDocs bool) error {
}()
}
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
if err := InitializeTrayRecoveryManager(); err != nil {
log.Warnf("TrayRecovery manager unavailable: %v", err)
} else {
trayRecoveryManager.WatchLoginctl(loginctlManager)
}
}()
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient

View File

@@ -1,93 +0,0 @@
package trayrecovery
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/godbus/dbus/v5"
)
const resumeDelay = 3 * time.Second
type Manager struct {
conn *dbus.Conn
stopChan chan struct{}
wg sync.WaitGroup
}
func NewManager() (*Manager, error) {
conn, err := dbus.ConnectSessionBus()
if err != nil {
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
}
m := &Manager{
conn: conn,
stopChan: make(chan struct{}),
}
// Run a startup scan after a delay — covers the case where the process
// was killed during suspend and restarted by systemd (Type=dbus).
// The fresh process never sees the PrepareForSleep true→false transition,
// so the loginctl watcher alone is not enough.
go m.scheduleRecovery()
return m, nil
}
// WatchLoginctl subscribes to loginctl session state changes and triggers
// tray recovery after resume from suspend (PrepareForSleep false transition).
// This handles the case where the process survives suspend.
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("tray-recovery")
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("tray-recovery")
wasSleeping := false
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
wasSleeping = true
continue
}
if wasSleeping {
wasSleeping = false
go m.scheduleRecovery()
}
}
}
}()
}
func (m *Manager) scheduleRecovery() {
select {
case <-time.After(resumeDelay):
m.recoverTrayItems()
case <-m.stopChan:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
if m.conn != nil {
m.conn.Close()
}
log.Info("TrayRecovery manager closed")
}

View File

@@ -1,262 +0,0 @@
package trayrecovery
import (
"context"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
)
const (
sniWatcherDest = "org.kde.StatusNotifierWatcher"
sniWatcherPath = "/StatusNotifierWatcher"
sniWatcherIface = "org.kde.StatusNotifierWatcher"
sniItemIface = "org.kde.StatusNotifierItem"
dbusIface = "org.freedesktop.DBus"
propsIface = "org.freedesktop.DBus.Properties"
probeTimeout = 300 * time.Millisecond
connProbeTimeout = 150 * time.Millisecond
batchSize = 30
)
var excludedPrefixes = []string{
"org.freedesktop.",
"org.gnome.",
"org.kde.StatusNotifier",
"com.canonical.AppMenu",
"org.mpris.",
"org.pipewire.",
"org.pulseaudio",
"fi.epitaph",
"quickshell",
"org.kde.quickshell",
}
func (m *Manager) recoverTrayItems() {
registeredItems := m.getRegisteredItems()
allNames := m.getDBusNames()
if allNames == nil {
return
}
registeredConnIDs := m.buildRegisteredConnIDs(registeredItems)
count := len(registeredItems)
log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count)
m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs)
m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs)
}
func (m *Manager) getRegisteredItems() []string {
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems")
if err != nil {
log.Warnf("TrayRecoveryService: failed to get registered items: %v", err)
return nil
}
switch v := variant.Value().(type) {
case []string:
return v
case []any:
items := make([]string, 0, len(v))
for _, elem := range v {
if s, ok := elem.(string); ok {
items = append(items, s)
}
}
return items
}
return nil
}
func (m *Manager) getDBusNames() []string {
var names []string
err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names)
if err != nil {
log.Warnf("TrayRecoveryService: failed to list bus names: %v", err)
return nil
}
return names
}
func (m *Manager) getNameOwner(name string) string {
var owner string
err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner)
if err != nil {
return ""
}
return owner
}
// buildRegisteredConnIDs resolves every registered SNI item (well-known name
// or :1.xxx connection ID) to a canonical connection ID. This prevents
// duplicates in both directions.
func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool {
connIDs := make(map[string]bool, len(registeredItems))
for _, item := range registeredItems {
name := extractName(item)
if strings.HasPrefix(name, ":1.") {
connIDs[name] = true
} else {
owner := m.getNameOwner(name)
if owner != "" {
connIDs[owner] = true
}
}
}
return connIDs
}
// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for
// unregistered SNI items and re-registers them.
func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
registeredRaw := strings.Join(registeredItems, "\n")
for _, name := range allNames {
if strings.HasPrefix(name, ":") {
continue
}
if strings.Contains(registeredRaw, name) {
continue
}
// Skip if this name's connection ID is already in the registered set
// (handles the case where the app registered via connection ID instead)
connForName := m.getNameOwner(name)
if connForName != "" && registeredConnIDs[connForName] {
continue
}
if isExcludedName(name) {
continue
}
short := shortName(name)
objectPaths := []string{
"/StatusNotifierItem",
"/org/ayatana/NotificationItem/" + short,
}
for _, objPath := range objectPaths {
if m.probeSNI(name, objPath, probeTimeout) {
m.registerSNI(name)
// Update set so the connection-ID section won't double-register this app
if connForName != "" {
registeredConnIDs[connForName] = true
}
break
}
}
}
}
// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered
// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an
// error instantly, so this is fast.
func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
registeredRaw := strings.Join(registeredItems, "\n")
registeredLower := strings.ToLower(registeredRaw)
var wg sync.WaitGroup
sem := make(chan struct{}, batchSize)
for _, name := range allNames {
if !strings.HasPrefix(name, ":1.") {
continue
}
if registeredConnIDs[name] {
continue
}
sem <- struct{}{}
wg.Add(1)
go func(conn string) {
defer wg.Done()
defer func() { <-sem }()
sniID := m.getSNIId(conn, connProbeTimeout)
if sniID == "" {
return
}
// Skip if an item with the same Id is already registered (case-insensitive)
if strings.Contains(registeredLower, strings.ToLower(sniID)) {
return
}
m.registerSNI(conn)
log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID)
}(name)
}
wg.Wait()
}
func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
obj := m.conn.Object(dest, dbus.ObjectPath(path))
var props map[string]dbus.Variant
err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props)
if err != nil {
return false
}
_, hasID := props["Id"]
return hasID
}
func (m *Manager) getSNIId(dest string, timeout time.Duration) string {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
obj := m.conn.Object(dest, "/StatusNotifierItem")
var variant dbus.Variant
err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant)
if err != nil {
return ""
}
id, _ := variant.Value().(string)
return id
}
func (m *Manager) registerSNI(name string) {
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name)
if call.Err != nil {
log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err)
return
}
log.Infof("TrayRecovery: re-registered %s", name)
}
func extractName(item string) string {
if idx := strings.IndexByte(item, '/'); idx != -1 {
return item[:idx]
}
return item
}
func shortName(name string) string {
parts := strings.Split(name, ".")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return name
}
func isExcludedName(name string) bool {
for _, prefix := range excludedPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}

View File

@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
if isGit {
return "dms-shell-git"
}
return "dms-shell"
return "dms-shell-bin"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit {
return "dms-git"

View File

@@ -434,7 +434,6 @@ Singleton {
property bool soundNewNotification: true
property bool soundVolumeChanged: true
property bool soundPluggedIn: true
property bool soundLogin: false
property int acMonitorTimeout: 0
property int acLockTimeout: 0

View File

@@ -242,7 +242,6 @@ var SPEC = {
soundsEnabled: { def: true },
useSystemSoundTheme: { def: false },
soundLogin: { def: false },
soundNewNotification: { def: true },
soundVolumeChanged: { def: true },
soundPluggedIn: { def: true },

View File

@@ -221,22 +221,10 @@ Item {
}
}
Timer {
id: loginSoundTimer
// Half a second delay before playing login sound, otherwise the sound may be cut off
// 50 is the minimum that seems to work, but 500 is safer
interval: 500
repeat: false
onTriggered: {
AudioService.playLoginSoundIfApplicable();
}
}
Component.onCompleted: {
dockRecreateDebounce.start();
// Force PolkitService singleton to initialize
PolkitService.polkitAvailable;
loginSoundTimer.start();
}
Loader {

View File

@@ -369,7 +369,9 @@ Item {
}
function previous(): void {
MprisController.previousOrRewind();
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous();
}
}
function next(): void {

View File

@@ -122,7 +122,7 @@ Item {
}
StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
text: I18n.tr("No recent clipboard entries found")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
@@ -181,7 +181,7 @@ Item {
}
StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
text: I18n.tr("No saved clipboard entries")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText

View File

@@ -60,12 +60,15 @@ DankModal {
}
function show() {
if (!clipboardAvailable) {
ToastService.showError(I18n.tr("Clipboard service not available"));
return;
}
open();
activeImageLoads = 0;
shouldHaveFocus = true;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {

View File

@@ -50,11 +50,14 @@ DankPopout {
}
function show() {
if (!clipboardAvailable) {
ToastService.showError(I18n.tr("Clipboard service not available"));
return;
}
open();
activeImageLoads = 0;
ClipboardService.reset();
if (clipboardAvailable)
ClipboardService.refresh();
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
@@ -119,10 +122,10 @@ DankPopout {
onBackgroundClicked: hide()
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
if (!shouldBeVisible) {
return;
if (clipboardAvailable)
ClipboardService.refresh();
}
ClipboardService.refresh();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item?.searchField) {

View File

@@ -31,7 +31,7 @@ Item {
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius

View File

@@ -132,7 +132,7 @@ DankModal {
modalWidth: 680
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
backgroundColor: Theme.surfaceContainer
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1

View File

@@ -311,7 +311,7 @@ FocusScope {
Item {
anchors.fill: parent
visible: !editMode && !(root.parentModal?.isClosing ?? false)
visible: !editMode
Item {
id: footerBar
@@ -737,6 +737,8 @@ FocusScope {
Item {
width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList {
id: resultsList
anchors.fill: parent

View File

@@ -324,8 +324,6 @@ Item {
height: 24
z: 100
visible: {
if (BlurService.enabled)
return false;
if (mainListView.contentHeight <= mainListView.height)
return false;
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
@@ -451,7 +449,7 @@ Item {
case "apps":
return "apps";
default:
return "search_off";
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
}
}
}
@@ -487,9 +485,9 @@ Item {
case "plugins":
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
case "apps":
return I18n.tr("No apps found");
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
default:
return I18n.tr("No results found");
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
}
}
}

View File

@@ -133,7 +133,7 @@ DankPopout {
QtObject {
id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
property bool isClosing: false
function hide() {
appDrawerPopout.close();

View File

@@ -34,7 +34,7 @@ PluginComponent {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
color: Theme.surfaceContainerHigh
DankActionButton {
anchors.top: parent.top
@@ -252,7 +252,7 @@ PluginComponent {
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceLight
color: Theme.surfaceContainerHighest
border.width: 1
border.color: Theme.outlineLight
opacity: 1.0

View File

@@ -33,7 +33,7 @@ Row {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
border.color: Theme.primarySelected
border.width: 0
radius: Theme.cornerRadius

View File

@@ -207,9 +207,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
border.width: modelData === AudioService.source ? 2 : 1
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left

View File

@@ -218,9 +218,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: modelData === AudioService.sink ? 2 : 1
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
DankRipple {
id: deviceRipple
@@ -397,9 +397,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: modelData === AudioService.sink ? 2 : 1
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left

View File

@@ -129,9 +129,8 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
@@ -165,9 +164,8 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent

View File

@@ -153,7 +153,7 @@ Item {
width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: modalVisible ? 1 : 0

View File

@@ -229,6 +229,7 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
border.width: 0
Component.onCompleted: {
if (!isConnected)
@@ -242,8 +243,8 @@ Rectangle {
if (isConnecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (deviceMouseArea.containsMouse)
return Theme.primaryHoverLight;
return Theme.surfaceLight;
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
}
border.color: {
@@ -251,9 +252,8 @@ Rectangle {
return Theme.warning;
if (isConnected)
return Theme.primary;
return Theme.outlineLight;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12);
}
border.width: (isConnecting || isConnected) ? 2 : 1
Row {
anchors.left: parent.left
@@ -490,9 +490,9 @@ Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
opacity: isInteractive ? 1 : 0.6
Row {

View File

@@ -79,9 +79,9 @@ Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Theme.surfaceLight
border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
border.width: modelData.mount === currentMountPath ? 2 : 1
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.mount === currentMountPath ? 2 : 0
Row {
anchors.left: parent.left

View File

@@ -308,9 +308,9 @@ Rectangle {
width: parent.width
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: isActive ? 2 : 1
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: Theme.primary
border.width: 0
Row {
id: wiredContentRow
@@ -565,9 +565,9 @@ Rectangle {
width: wifiContent.width
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
border.width: wifiDelegate.isConnected ? 2 : 1
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
id: wifiContentRow

View File

@@ -1,5 +1,4 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Modules.Plugins
import qs.Services
@@ -11,8 +10,6 @@ BasePill {
property bool batteryPopupVisible: false
property var popoutTarget: null
property real touchpadAccumulator: 0
readonly property int barPosition: {
switch (axis?.edge) {
case "top":
@@ -122,44 +119,5 @@ BasePill {
battery.triggerRipple(this, mouse.x, mouse.y);
toggleBatteryPopup();
}
onWheel: wheel => {
var delta = wheel.angleDelta.y;
if (delta === 0)
return;
// Check if this is a touchpad
if (delta !== 120 && delta !== -120) {
touchpadAccumulator += delta;
console.info("Acc: "+touchpadAccumulator);
if (Math.abs(touchpadAccumulator) < 500)
return;
delta = touchpadAccumulator;
touchpadAccumulator = 0;
}
console.info("Trigger! Delta: "+delta)
// This is after the other delta checks so it only shows on valid Y scroll
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return;
}
// Get list of profiles, and current index
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
// Step once based on mouse wheel direction
if (delta > 0) index += 1;
else index -= 1;
// Already at end of list, can't go further
if (index < 0 || index >= profiles.length) return;
// Set new profile
PowerProfiles.profile = profiles[index];
if (PowerProfiles.profile !== profiles[index]) {
ToastService.showError("Failed to set power profile");
}
}
}
}

View File

@@ -102,7 +102,7 @@ BasePill {
StyledTextMetrics {
id: cpuBaseline
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
text: "100%"
text: "88%"
}
StyledTextMetrics {

View File

@@ -17,7 +17,7 @@ BasePill {
property int availableWidth: 400
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
property Toplevel activeWindow: null
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse
property bool isAutoHideBar: false
@@ -38,44 +38,10 @@ BasePill {
return 0;
}
function updateActiveWindow() {
const active = ToplevelManager.activeToplevel;
if (!active) {
// Only clear if our tracked window is no longer alive
if (activeWindow) {
const alive = ToplevelManager.toplevels?.values;
if (alive && !Array.from(alive).some(t => t === activeWindow))
activeWindow = null;
}
return;
}
if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) {
activeWindow = active;
}
// else: active window is on a different screen so keep the previous value
}
Component.onCompleted: {
updateActiveWindow();
updateDesktopEntry();
}
Connections {
target: ToplevelManager
function onActiveToplevelChanged() {
root.updateActiveWindow();
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
root.updateActiveWindow();
}
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {

View File

@@ -99,7 +99,7 @@ BasePill {
if (isMouseWheelY) {
if (deltaY > 0) {
MprisController.previousOrRewind();
activePlayer.previous();
} else {
activePlayer.next();
}
@@ -107,7 +107,7 @@ BasePill {
scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
if (scrollAccumulatorY > 0) {
MprisController.previousOrRewind();
activePlayer.previous();
} else {
activePlayer.next();
}
@@ -214,7 +214,7 @@ BasePill {
if (mouse.button === Qt.LeftButton) {
activePlayer.togglePlaying();
} else if (mouse.button === Qt.MiddleButton) {
MprisController.previousOrRewind();
activePlayer.previous();
} else if (mouse.button === Qt.RightButton) {
activePlayer.next();
}
@@ -370,7 +370,11 @@ BasePill {
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind()
onClicked: {
if (activePlayer) {
activePlayer.previous();
}
}
}
}

View File

@@ -20,46 +20,6 @@ Item {
property var blurBarWindow: null
property var hyprlandOverviewLoader: null
property var parentScreen: null
readonly property real _leftMargin: {
if (isVertical)
return 0;
root.x;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _rightMargin: {
if (isVertical)
return 0;
root.x;
root.width;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _topMargin: {
if (!isVertical)
return 0;
root.y;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _bottomMargin: {
if (!isVertical)
return 0;
root.y;
root.height;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
@@ -579,60 +539,6 @@ Item {
});
}
function switchToWorkspaceByModelData(data) {
if (!data)
return;
if (root.useExtWorkspace && (data.id || data.name)) {
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
return;
}
switch (CompositorService.compositor) {
case "niri":
if (data.idx !== undefined)
NiriService.switchToWorkspace(data.idx);
break;
case "hyprland":
if (data.id)
Hyprland.dispatch(`workspace ${data.id}`);
break;
case "dwl":
if (data.tag !== undefined)
DwlService.switchToTag(root.screenName, data.tag);
break;
case "sway":
case "scroll":
case "miracle":
if (data.num)
try {
I3.dispatch(`workspace number ${data.num}`);
} catch (_) {}
break;
}
}
function findClosestWorkspaceIndex(localX, localY) {
if (workspaceRepeater.count === 0)
return -1;
let closestIdx = -1;
let closestDist = Infinity;
for (let i = 0; i < workspaceRepeater.count; i++) {
const item = workspaceRepeater.itemAt(i);
if (!item)
continue;
const center = item.mapToItem(root, item.width / 2, item.height / 2);
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
if (dist < closestDist) {
closestDist = dist;
closestIdx = i;
}
}
return closestIdx;
}
function switchWorkspace(direction) {
if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces();
@@ -846,15 +752,8 @@ Item {
}
MouseArea {
id: edgeMouseArea
z: -1
x: -root._leftMargin
y: -root._topMargin
width: root.width + root._leftMargin + root._rightMargin
height: root.height + root._topMargin + root._bottomMargin
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
acceptedButtons: Qt.RightButton
property real touchpadAccumulator: 0
property real mouseAccumulator: 0
@@ -867,20 +766,12 @@ Item {
}
onClicked: mouse => {
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
switch (mouse.button) {
case Qt.RightButton:
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
}
break;
case Qt.LeftButton:
const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
if (idx >= 0)
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
break;
}
}

View File

@@ -487,7 +487,17 @@ Item {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind()
onClicked: {
if (!activePlayer) {
return;
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
}
}
}
}

View File

@@ -145,7 +145,14 @@ Card {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind()
onClicked: {
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}

View File

@@ -1338,7 +1338,7 @@ Item {
enabled: MprisController.activePlayer?.canGoPrevious ?? false
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.previousOrRewind()
onClicked: MprisController.activePlayer?.previous()
}
}

View File

@@ -91,16 +91,6 @@ Item {
visible: AudioService.gsettingsAvailable
}
SettingsToggleRow {
tab: "sounds"
tags: ["sound", "login", "startup", "boot"]
settingKey: "soundLogin"
text: I18n.tr("Login")
description: I18n.tr("Play sound after logging in")
checked: SettingsData.soundLogin
onToggled: checked => SettingsData.set("soundLogin", checked)
}
SettingsToggleRow {
tab: "sounds"
tags: ["sound", "notification", "new"]

View File

@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Widgets
import qs.Services
@@ -51,14 +52,15 @@ Column {
height: implicitHeight
spacing: Theme.spacingM
Row {
RowLayout {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: root.titleIcon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
StyledText {
@@ -66,7 +68,7 @@ Column {
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
}
@@ -979,26 +981,10 @@ Column {
Repeater {
model: [
{
label: I18n.tr("Percentage"),
mode: 0,
icon: "percent"
},
{
label: I18n.tr("Total"),
mode: 1,
icon: "storage"
},
{
label: I18n.tr("Remaining"),
mode: 2,
icon: "hourglass_empty"
},
{
label: I18n.tr("Remaining / Total"),
mode: 3,
icon: "pie_chart"
}
{ label: I18n.tr("Percentage"), mode: 0, icon: "percent" },
{ label: I18n.tr("Total"), mode: 1, icon: "storage" },
{ label: I18n.tr("Remaining"), mode: 2, icon: "hourglass_empty" },
{ label: I18n.tr("Remaining / Total"), mode: 3, icon: "pie_chart" }
]
delegate: Rectangle {
@@ -1330,7 +1316,20 @@ Column {
id: longestControlCenterLabelMetrics
font.pixelSize: Theme.fontSizeSmall
text: {
const labels = [I18n.tr("Network"), I18n.tr("VPN"), I18n.tr("Bluetooth"), I18n.tr("Audio"), I18n.tr("Volume"), I18n.tr("Microphone"), I18n.tr("Microphone Volume"), I18n.tr("Brightness"), I18n.tr("Brightness Value"), I18n.tr("Battery"), I18n.tr("Printer"), I18n.tr("Screen Sharing")];
const labels = [
I18n.tr("Network"),
I18n.tr("VPN"),
I18n.tr("Bluetooth"),
I18n.tr("Audio"),
I18n.tr("Volume"),
I18n.tr("Microphone"),
I18n.tr("Microphone Volume"),
I18n.tr("Brightness"),
I18n.tr("Brightness Value"),
I18n.tr("Battery"),
I18n.tr("Printer"),
I18n.tr("Screen Sharing")
];
let longest = "";
for (let i = 0; i < labels.length; i++) {
if (labels[i].length > longest.length)
@@ -1341,7 +1340,6 @@ Column {
}
Repeater {
id: groupRepeater
model: controlCenterContextMenu.controlCenterGroups
delegate: Item {
@@ -1571,6 +1569,8 @@ Column {
}
}
}
id: groupRepeater
}
}
}

View File

@@ -26,7 +26,6 @@ Singleton {
property var powerUnplugSound: null
property var normalNotificationSound: null
property var criticalNotificationSound: null
property var loginSound: null
property real notificationsVolume: 1.0
property bool notificationsAudioMuted: false
@@ -68,16 +67,6 @@ Singleton {
}
}
// Used in playLoginSoundIfApplicable()
Process {
id: loginSoundChecker
onExited: (exitCode) => {
if (exitCode === 0) {
playLoginSound();
}
}
}
function getAvailableSinks() {
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
@@ -406,7 +395,7 @@ EOFCONFIG
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
const script = `
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
found=0
case "$event_key" in
@@ -468,8 +457,7 @@ EOFCONFIG
"power-plug": "../assets/sounds/plasma/power-plug.wav",
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
"message": "../assets/sounds/freedesktop/message.wav",
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
};
const specialConditions = {
@@ -563,10 +551,6 @@ EOFCONFIG
criticalNotificationSound.destroy();
criticalNotificationSound = null;
}
if (loginSound) {
loginSound.destroy();
loginSound = null;
}
}
function createSoundPlayers() {
@@ -638,19 +622,6 @@ EOFCONFIG
}
}
`, root, "AudioService.CriticalNotificationSound");
const loginPath = getSoundPath("desktop-login");
loginSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${loginPath}"
audioOutput: AudioOutput {
${deviceProperty}volume: notificationsVolume
}
}
`, root, "AudioService.LoginSound");
} catch (e) {
console.warn("AudioService: Error creating sound players:", e);
}
@@ -690,31 +661,6 @@ EOFCONFIG
criticalNotificationSound.play();
}
function playLoginSound() {
if (!soundsAvailable || !loginSound || notificationsAudioMuted || isMediaPlaying()) {
return;
}
loginSound.play();
}
function playLoginSoundIfApplicable() {
if (SettingsData.soundsEnabled && SettingsData.soundLogin && !notificationsAudioMuted) {
// plays login sound on session start, but only if a specific file doesn't exist,
// to prevent it from playing on every DMS restart during the session
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
if (!runtimeDir) return;
const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;
// if file doesn't exist, touch it (0)
// If it exists, do nothing (1)
loginSoundChecker.command = ["sh", "-c", `[ ! -f ${loginFile} ] && touch ${loginFile}`];
loginSoundChecker.running = true;
}
}
function playVolumeChangeSoundIfEnabled() {
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
playVolumeChangeSound();

View File

@@ -3,16 +3,13 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland // ! Import is needed despite what qmlls says
import qs.Common
Singleton {
id: root
property bool quickshellSupported: false
property bool compositorSupported: false
property bool available: quickshellSupported && compositorSupported
property bool available: false
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
readonly property color borderColor: {
@@ -75,27 +72,6 @@ Singleton {
region.destroy();
}
Process {
id: blurProbe
running: false
command: ["dms", "blur", "check"]
stdout: StdioCollector {
onStreamFinished: {
root.compositorSupported = text.trim() === "supported";
if (root.compositorSupported)
console.info("BlurService: Compositor supports ext-background-effect-v1");
else
console.info("BlurService: Compositor does not support ext-background-effect-v1");
}
}
onExited: exitCode => {
if (exitCode !== 0)
console.warn("BlurService: blur probe failed with code:", exitCode);
}
}
Component.onCompleted: {
try {
const test = Qt.createQmlObject(`
@@ -103,9 +79,8 @@ Singleton {
Region { radius: 0 }
`, root, "BlurAvailabilityTest");
test.destroy();
quickshellSupported = true;
console.info("BlurService: Quickshell blur support available");
blurProbe.running = true;
available = true;
console.info("BlurService: Initialized with blur support");
} catch (e) {
console.info("BlurService: BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
}

View File

@@ -255,12 +255,6 @@ Singleton {
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
}
onClipboardAvailableChanged: {
if (!clipboardAvailable || refCount <= 0)
return;
refresh();
}
Connections {
target: DMSService
enabled: root.refCount > 0

View File

@@ -10,20 +10,4 @@ Singleton {
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
Timer {
interval: 1000
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: root.activePlayer?.positionChanged()
}
function previousOrRewind(): void {
if (!activePlayer)
return;
if (activePlayer.position > 8 && activePlayer.canSeek)
activePlayer.position = 0;
else if (activePlayer.canGoPrevious)
activePlayer.previous();
}
}

View File

@@ -89,7 +89,7 @@ Row {
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
height: root.buttonHeight
color: selected ? Theme.buttonBg : Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
color: selected ? Theme.buttonBg : Theme.surfaceVariant
border.color: "transparent"
border.width: 0

View File

@@ -266,7 +266,7 @@ PanelWindow {
scale: shouldBeVisible ? 1 : 0.9
property bool childHovered: false
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
Rectangle {
id: background
@@ -286,7 +286,7 @@ PanelWindow {
level: Theme.elevationLevel3
fallbackOffset: 6
targetRadius: Theme.cornerRadius
targetColor: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
targetColor: Theme.surfaceContainer
borderColor: Theme.outlineMedium
borderWidth: 1
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"

View File

@@ -576,6 +576,14 @@ Item {
}
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth
}
Loader {
id: contentLoader
anchors.fill: parent
@@ -583,21 +591,6 @@ Item {
asynchronous: false
}
}
Rectangle {
width: parent.width
height: parent.height
x: contentWrapper.x
y: contentWrapper.y
opacity: contentWrapper.opacity
scale: contentWrapper.scale
visible: contentWrapper.visible
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth
z: 100
}
}
Item {

View File

@@ -238,7 +238,7 @@ Rectangle {
width: fieldContent.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius - 2
color: Theme.surfaceLight
color: Theme.surfaceContainerHigh
border.width: 1
border.color: Theme.outlineLight
@@ -272,9 +272,7 @@ Rectangle {
checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null
onToggled: checked => {
VPNService.updateConfig(profile.uuid, {
autoconnect: checked
});
VPNService.updateConfig(profile.uuid, {autoconnect: checked});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -86,13 +86,13 @@ def create_poeditor_json(translations):
references.append(ref)
contexts = sorted(data['contexts']) if data['contexts'] else []
comment = " | ".join(contexts) if contexts else ""
context_str = " | ".join(contexts) if contexts else term
entry = {
"term": term,
"context": term,
"context": context_str,
"reference": ", ".join(references),
"comment": comment
"comment": ""
}
poeditor_data.append(entry)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2112,26 +2112,6 @@
],
"icon": "history"
},
{
"section": "rememberLastQuery",
"label": "Remember Last Query",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"autofill",
"drawer",
"last",
"launcher",
"menu",
"opened",
"query",
"remember",
"remembered",
"search",
"start"
],
"description": "Autofill last remembered query when opened"
},
{
"section": "searchAppActions",
"label": "Search App Actions",
@@ -2398,47 +2378,6 @@
],
"icon": "schedule"
},
{
"section": "blurEnabled",
"label": "Background Blur",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"alert",
"alerts",
"appearance",
"background",
"bars",
"behind",
"blur",
"colors",
"compositor",
"config",
"configuration",
"configure",
"frosted",
"glass",
"look",
"modals",
"notif",
"notifications",
"notifs",
"panel",
"popouts",
"requires",
"scheme",
"setup",
"statusbar",
"style",
"support",
"taskbar",
"theme",
"topbar",
"transparency"
],
"icon": "blur_on",
"description": "Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."
},
{
"section": "barElevationEnabled",
"label": "Bar Shadows",
@@ -2466,49 +2405,6 @@
],
"description": "Shadow elevation on bars and panels"
},
{
"section": "blurBorderColor",
"label": "Blur Border Color",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"appearance",
"around",
"blur",
"blurred",
"border",
"color",
"colors",
"colour",
"edge",
"hue",
"look",
"outline",
"scheme",
"style",
"surfaces",
"theme",
"tint"
],
"description": "Border color around blurred surfaces"
},
{
"section": "blurBorderOpacity",
"label": "Blur Border Opacity",
"tabIndex": 10,
"category": "Theme & Colors",
"keywords": [
"appearance",
"blur",
"border",
"colors",
"look",
"opacity",
"scheme",
"style",
"theme"
]
},
{
"section": "niriLayoutBorderSize",
"label": "Border Size",
@@ -4488,6 +4384,27 @@
],
"description": "Automatically lock the screen when DMS starts"
},
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"automatic",
"automatically",
"before",
"lock",
"login",
"password",
"prepares",
"screen",
"security",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{
"section": "lockScreenNotificationMode",
"label": "Notification Display",
@@ -5101,26 +5018,6 @@
],
"description": "Play sounds for system events"
},
{
"section": "soundLogin",
"label": "Login",
"tabIndex": 15,
"category": "Sounds",
"keywords": [
"after",
"audio",
"boot",
"effects",
"logging",
"login",
"play",
"sfx",
"sound",
"sounds",
"startup"
],
"description": "Play sound after logging in"
},
{
"section": "soundNewNotification",
"label": "New Notification",
@@ -6510,27 +6407,6 @@
"icon": "schedule",
"description": "Gradually fade the screen before locking with a configurable grace period"
},
{
"section": "lockBeforeSuspend",
"label": "Lock before suspend",
"tabIndex": 21,
"category": "Power & Sleep",
"keywords": [
"automatically",
"before",
"energy",
"lock",
"power",
"prepares",
"screen",
"security",
"shutdown",
"sleep",
"suspend",
"system"
],
"description": "Automatically lock the screen when the system prepares to suspend"
},
{
"section": "fadeToLockGracePeriod",
"label": "Lock fade grace period",

View File

@@ -1182,13 +1182,6 @@
"reference": "",
"comment": ""
},
{
"term": "Applying authentication changes…",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Apps",
"translation": "",
@@ -1385,34 +1378,6 @@
"reference": "",
"comment": ""
},
{
"term": "Authentication changes applied.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Authentication changes apply automatically.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Authentication changes need sudo. Opening terminal so you can use password or fingerprint.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Authentication error - try again",
"translation": "",
@@ -1553,13 +1518,6 @@
"reference": "",
"comment": ""
},
{
"term": "Autofill last remembered query when opened",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Automatic Color Mode",
"translation": "",
@@ -1714,13 +1672,6 @@
"reference": "",
"comment": ""
},
{
"term": "Background Blur",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Background Opacity",
"translation": "",
@@ -1728,13 +1679,6 @@
"reference": "",
"comment": ""
},
{
"term": "Background authentication sync failed. Trying terminal mode.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Background image",
"translation": "",
@@ -1931,20 +1875,6 @@
"reference": "",
"comment": ""
},
{
"term": "Blur Border Color",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Blur Border Opacity",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Blur Wallpaper Layer",
"translation": "",
@@ -1959,13 +1889,6 @@
"reference": "",
"comment": ""
},
{
"term": "Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Blur wallpaper when niri overview is open",
"translation": "",
@@ -2015,13 +1938,6 @@
"reference": "",
"comment": ""
},
{
"term": "Border color around blurred surfaces",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Border with BG",
"translation": "",
@@ -2373,7 +2289,7 @@
"comment": ""
},
{
"term": "Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.",
"term": "Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.",
"translation": "",
"context": "",
"reference": "",
@@ -3628,7 +3544,7 @@
{
"term": "Custom",
"translation": "",
"context": "blur border color | shadow color option | theme category option",
"context": "shadow color option | theme category option",
"reference": "",
"comment": ""
},
@@ -3801,7 +3717,7 @@
"comment": ""
},
{
"term": "DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Authentication changes apply automatically and may open a terminal when sudo authentication is required.",
"term": "DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.",
"translation": "",
"context": "",
"reference": "",
@@ -4872,7 +4788,7 @@
"comment": ""
},
{
"term": "Enable fingerprint or security key for DMS Greeter. Authentication changes apply automatically.",
"term": "Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.",
"translation": "",
"context": "",
"reference": "",
@@ -4920,13 +4836,6 @@
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.",
"translation": "",
@@ -4935,7 +4844,7 @@
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.",
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.",
"translation": "",
"context": "",
"reference": "",
@@ -4948,6 +4857,13 @@
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Register a key or update your U2F config.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but security-key availability could not be confirmed.",
"translation": "",
@@ -5893,27 +5809,6 @@
"reference": "",
"comment": ""
},
{
"term": "Fingerprint error",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Fingerprint error: %1",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Fingerprint not recognized (%1/%2). Please try again or use password.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.",
"translation": "",
@@ -7244,13 +7139,6 @@
"reference": "",
"comment": ""
},
{
"term": "Incorrect password - try again",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Indicator Style",
"translation": "",
@@ -7314,13 +7202,6 @@
"reference": "",
"comment": ""
},
{
"term": "Insert your security key...",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Install",
"translation": "",
@@ -8049,13 +7930,6 @@
"reference": "",
"comment": ""
},
{
"term": "Lock screen authentication changes apply automatically and may open a terminal when sudo authentication is required.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Locked",
"translation": "",
@@ -8441,13 +8315,6 @@
"reference": "",
"comment": ""
},
{
"term": "Maximum fingerprint attempts reached. Please use password.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Maximum number of clipboard entries to keep",
"translation": "",
@@ -10264,7 +10131,7 @@
{
"term": "Outline",
"translation": "",
"context": "blur border color | outline color",
"context": "outline color",
"reference": "",
"comment": ""
},
@@ -11195,7 +11062,7 @@
{
"term": "Primary",
"translation": "",
"context": "blur border color | button color option | color option | primary color | shadow color option | tile color option",
"context": "button color option | color option | primary color | shadow color option | tile color option",
"reference": "",
"comment": ""
},
@@ -11612,13 +11479,6 @@
"reference": "",
"comment": ""
},
{
"term": "Remember Last Query",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Remember last session",
"translation": "",
@@ -11745,13 +11605,6 @@
"reference": "",
"comment": ""
},
{
"term": "Requires a newer version of Quickshell",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Requires night mode support",
"translation": "",
@@ -11997,6 +11850,20 @@
"reference": "",
"comment": ""
},
{
"term": "Run Sync to apply.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Run User Templates",
"translation": "",
@@ -12392,7 +12259,7 @@
{
"term": "Secondary",
"translation": "",
"context": "blur border color | button color option | color option | secondary color | tile color option",
"context": "button color option | color option | secondary color | tile color option",
"reference": "",
"comment": ""
},
@@ -14188,13 +14055,6 @@
"reference": "",
"comment": ""
},
{
"term": "Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.",
"translation": "",
@@ -14202,13 +14062,6 @@
"reference": "",
"comment": ""
},
{
"term": "Terminal fallback opened. Complete authentication setup there; it will close automatically when done.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Terminal fallback opened. Complete sync there; it will close automatically when done.",
"translation": "",
@@ -14223,13 +14076,6 @@
"reference": "",
"comment": ""
},
{
"term": "Terminal opened. Complete authentication setup there; it will close automatically when done.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Terminal opened. Complete sync authentication there; it will close automatically when done.",
"translation": "",
@@ -14282,7 +14128,7 @@
{
"term": "Text Color",
"translation": "",
"context": "blur border color | shadow color option",
"context": "shadow color option",
"reference": "",
"comment": ""
},
@@ -14643,13 +14489,6 @@
"reference": "",
"comment": ""
},
{
"term": "Too many attempts - locked out",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Too many failed attempts - account may be locked",
"translation": "",
@@ -14734,13 +14573,6 @@
"reference": "",
"comment": ""
},
{
"term": "Touch your security key...",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Transform",
"translation": "",
@@ -14846,6 +14678,20 @@
"reference": "",
"comment": ""
},
{
"term": "Type to search",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Type to search apps",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Type to search files",
"translation": "",