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

Compare commits

..

25 Commits

Author SHA1 Message Date
bbedward
f2df53afcd colorpicker: re-use Wayland buffer pools 2026-04-09 12:08:43 -04:00
Thomas Kroll
4179fcee83 fix(privacy): detect screen casting on Niri via PipeWire (#2185)
Screen sharing was not detected by PrivacyService on Niri because:

1. Niri creates the screencast as a Stream/Output/Video node, but
   screensharingActive only checked PwNodeType.VideoSource nodes.

2. looksLikeScreencast() only inspected application.name and
   node.name, missing Niri's node which has an empty application.name
   but identifies itself via media.name (niri-screen-cast-src).

Add Stream/Output/Video to the checked media classes and include
media.name in the screencast heuristic. Also add a forward-compatible
check for NiriService.hasActiveCast for when Niri gains cast tracking
in its IPC.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:50:39 -04:00
Andrey Yugai
a0c9af1ee7 feature: persist last active player (#2184) 2026-04-09 11:30:04 -04:00
Thomas Kroll
049266271a fix(system-update): popout first-click and AUR package listing (#2183)
* fix(system-update): open popout on first click

The SystemUpdate widget required two clicks to open its popout.

On the first click, the LazyLoader was activated but popoutTarget
(bound to the loader's item) was still null in the MouseArea handler,
so setTriggerPosition was never called. The popout's open() then
returned early because screen was unset.

Restructure the onClicked handler to call setTriggerPosition directly
on the loaded item (matching the pattern used by Clock, Clipboard, and
other bar widgets) and use PopoutManager.requestPopout() instead of
toggle() for consistent popout management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(system-update): include AUR packages in update list

When paru or yay is the package manager, the update list only showed
official repo packages (via checkupdates or -Qu) while the upgrade
command (paru/yay -Syu) also processes AUR packages. This mismatch
meant AUR updates appeared as a surprise during the upgrade.

Combine the repo update listing with the AUR helper's -Qua flag so
both official and AUR packages are shown in the popout before the
user triggers the upgrade. The output format is identical for both
sources, so the existing parser works unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:49:15 -04:00
Ron Harel
0eabda3164 Improve seek and scrub indicator/ animations in the media controls widget. (#2181) 2026-04-09 10:35:56 -04:00
bbedward
32c063aab8 Revert "qml: cut down on inline components for performance"
This reverts commit f6e590a518.
2026-04-07 15:22:46 -04:00
Ron Harel
37f92677cf Make the system tray overflow popup optional and have the widget expand inline as an alternative. (#2171) 2026-04-07 11:13:32 -04:00
Ron Harel
13e8130858 Add adaptive media width setting. (#2165) 2026-04-07 11:07:36 -04:00
bbedward
f6e590a518 qml: cut down on inline components for performance 2026-04-07 10:57:11 -04:00
bbedward
3194fc3fbe core: allow RO commands to run as root 2026-04-06 18:19:17 -04:00
bbedward
3318864ece clipboard: make CLI keep CL item in-memory again 2026-04-06 16:09:10 -04:00
Iris
e224417593 feature: add login sound functionality and settings entry (#2155)
* added login sound functionality and settings entry

* Removed debug warning that was accidentally left in

* loginSound is off by default, and fixed toggle not working

* Prevent login sound from playing in the same session

---------

Co-authored-by: Iris <iris@raidev.eu>
2026-04-06 14:11:00 -04:00
Marcus Ramberg
3f7f6c5d2c core(doctor): show all detected terminals (#2163) 2026-04-06 14:09:15 -04:00
bbedward
0b88055742 clipboard: fix reliability of modal/popout 2026-04-06 10:30:39 -04:00
bbedward
2b0826e397 core: migrate to dms-shell arch package 2026-04-06 10:09:55 -04:00
bbedward
7db04c9660 i18n: use comments instead of context, sync 2026-04-06 09:41:45 -04:00
Al- Amin
14d1e1d985 fix: Add match rule for new version of Gnome Calculator app ID (#2157) 2026-04-06 09:11:26 -04:00
Dimariqe
903ab1e61d fix: add TrayRecoveryService with bidirectional SNI dedup (Go server) (#2137)
* fix: add TrayRecoveryService with bidirectional SNI dedup (Go server)

Add TrayRecoveryService manager that re-registers lost tray icons after
resume from suspend via native DBus scanning in the Go server.

The service resolves every registered SNI item (both well-known names and
:1.xxx connection IDs) to a canonical connection ID, building a unified
registeredConnIDs set before either scan section runs. This prevents
duplicates in both directions:

- If an app registered via well-known name, the connection-ID section
  skips its :1.xxx entry.
- If an app registered via connection ID, the well-known-name section
  skips its well-known name (checked through registeredConnIDs).
- After successfully registering via well-known name, registeredConnIDs
  is updated immediately so the connection-ID section won't probe the
  same app in the same run.

A startup scan (3 s delay) covers the common case where the DMS process
is killed during suspend and restarted by systemd (Type=dbus), so the
loginctl PrepareForSleep watcher alone is not sufficient. The startup
scan is harmless on a normal boot — it finds all items already registered
and exits early.

Go port of quickshell commit 1470aa3.

* fix: 'interface{}' can be replaced by 'any'

* TrayRecoveryService: Remove objPath parameter from registerSNI
2026-04-06 09:03:36 -04:00
Walid Salah
5982655539 Make focused app widget only show focused app on the current screen (#2152) 2026-04-05 10:53:18 -04:00
Aaron Tulino
1021a210cf Change power profile by scrolling battery (#2142)
Scrolling up "increases" the profile (Power Saver -> Balanced -> Performance). Supports touchpad.
2026-04-03 12:04:00 -04:00
bbedward
e34edb15bb i18n: sync 2026-04-03 11:56:00 -04:00
dev
61ee5f4336 Update CpuMonitor.qml to reserve enough widget space for the widest number "100" instead of "88" (#2135)
My bar kept shifting around by a few pixels every time I hit 100% cpu usage
2026-04-02 15:26:56 -04:00
Phil Jackson
ce2a92ec27 feat: rewind to track start on previous when past 8 seconds (#2136)
* feat: rewind to track start on previous when past 8 seconds

Adds MprisController.previousOrRewind() which rewinds the current track
to position 0 if more than 8 seconds in (with canSeek check), and falls
back to previous() otherwise — matching traditional media player behaviour.

All previous() call sites across Media.qml, MediaPlayerTab.qml,
MediaOverviewCard.qml, LockScreenContent.qml and DMSShellIPC.qml
are updated to use the new shared function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: poll position in MprisController for previousOrRewind accuracy

Without a polling timer, activePlayer.position is never updated in
contexts that don't display a seekbar (e.g. the DankBar widget), causing
the position > 8 check in previousOrRewind() to always see 0 and fall
through to previous() instead of rewinding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 15:26:36 -04:00
Al- Amin
66ce79b9bf fix:update resizeactive binding to include height to make it work (#2126) 2026-04-01 09:19:01 -04:00
Al- Amin
30dd640314 fix:add window rule for the new version of Gnome Calculator (#2125) 2026-04-01 09:18:56 -04:00
81 changed files with 23140 additions and 25547 deletions

View File

@@ -820,10 +820,14 @@ func checkOptionalDependencies() []checkResult {
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
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})
} else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
}
networkResult, err := network.DetectNetworkStack()

View File

@@ -109,16 +109,41 @@ func updateArchLinux() error {
}
var packageName string
if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
var isAUR bool
if isArchPackageInstalled("dms-shell") {
packageName = "dms-shell"
} 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: Neither dms-shell-bin nor dms-shell-git package found.")
fmt.Println("Info: No dms-shell package found.")
fmt.Println("Info: Falling back to git-based update method...")
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,6 +5,7 @@ package main
import (
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
@@ -30,7 +31,9 @@ func init() {
}
func main() {
if os.Geteuid() == 0 {
clipboard.MaybeServeAndExit()
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.")
}

View File

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

View File

@@ -7,6 +7,22 @@ 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,7 +1,6 @@
package clipboard
import (
"bytes"
"fmt"
"io"
"os"
@@ -13,66 +12,142 @@ 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 CopyReader(bytes.NewReader(data), mimeType, false, false)
return copyForkCached(data, mimeType, false)
}
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
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 serveClipboard(data, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
return copyForkCached(data, mimeType, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground {
return copyFork(data, mimeType, pasteOnce)
if foreground {
buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
}
return copyServeReader(data, mimeType, pasteOnce)
return copyFork(data, mimeType, pasteOnce)
}
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
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:]...)
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
cmd := exec.Command(os.Args[0])
cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
cmd.Env = append(os.Environ(),
envServe+"=1",
envMime+"="+mimeType,
)
if pasteOnce {
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
}
cmd.Env = append(cmd.Env, extra...)
return cmd
}
func waitReady(cmd *exec.Cmd) error {
stdout, err := cmd.StdoutPipe()
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
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
return waitReady(cmd)
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)
}
@@ -83,50 +158,22 @@ 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)
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
return nil
}
func signalReady() {
if os.Getenv("DMS_CLIP_FORKED") == "" {
if os.Getenv(envServe) == "" {
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{}
@@ -147,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("")
if err != nil {
return fmt.Errorf("wayland connect: %w", err)
@@ -189,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
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")
}
@@ -233,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
cancelled := make(chan struct{})
pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd)
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
_, _ = file.Write(data)
select {
case pasted <- struct{}{}:
default:
@@ -266,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
select {
case <-cancelled:
return nil
case err := <-sendErr:
return err
case <-pasted:
if pasteOnce {
return nil
@@ -521,12 +558,10 @@ 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")
}
@@ -554,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd)
_ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close()
if data, ok := offerMap[e.MimeType]; ok {
file.Write(data)
_, _ = file.Write(data)
}
select {

View File

@@ -39,11 +39,10 @@ type LayerSurface struct {
wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport
wlPool *client.ShmPool
wlBuffer *client.Buffer
bufferBusy bool
oldPool *client.ShmPool
oldBuffer *client.Buffer
wlPools [2]*client.ShmPool
wlBuffers [2]*client.Buffer
slotBusy [2]bool
needsRedraw bool
scopyBuffer *client.Buffer
configured bool
hidden bool
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
break
}
p.flushRedraws()
p.checkDone()
}
@@ -164,6 +164,15 @@ func (p *Picker) checkDone() {
}
}
func (p *Picker) flushRedraws() {
for _, ls := range p.surfaces {
if !ls.needsRedraw {
continue
}
p.redrawSurface(ls)
}
}
func (p *Picker) connect() error {
display, err := client.Connect("")
if err != nil {
@@ -507,47 +516,45 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
}
func (p *Picker) redrawSurface(ls *LayerSurface) {
slot := ls.state.FrontIndex()
if ls.slotBusy[slot] {
ls.needsRedraw = true
return
}
var renderBuf *ShmBuffer
if ls.hidden {
switch {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly()
} else {
default:
renderBuf = ls.state.Redraw()
}
if renderBuf == nil {
return
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
ls.oldBuffer = nil
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
ls.oldPool = nil
ls.needsRedraw = false
if ls.wlPools[slot] == nil {
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPools[slot] = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffers[slot] = wlBuffer
s := slot
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
ls.slotBusy[s] = false
})
}
ls.oldPool = ls.wlPool
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
ls.slotBusy[slot] = true
logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 {
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
}
_ = ls.wlSurface.SetBufferScale(bufferScale)
}
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit()
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
p.activeSurface.needsRedraw = true
})
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
return
}
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface)
p.activeSurface.needsRedraw = true
})
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy()
}
if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
}
if ls.oldPool != nil {
ls.oldPool.Destroy()
}
if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
for i := range ls.wlBuffers {
if ls.wlBuffers[i] != nil {
ls.wlBuffers[i].Destroy()
}
if ls.wlPools[i] != nil {
ls.wlPools[i].Destroy()
}
}
if ls.viewport != nil {
ls.viewport.Destroy()

View File

@@ -274,6 +274,12 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
return s.renderBufs[s.front]
}
func (s *SurfaceState) FrontIndex() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.front
}
func (s *SurfaceState) SwapBuffers() {
s.mu.Lock()
s.front ^= 1

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%
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = SUPER, mouse:272, Move window, movewindow

View File

@@ -94,6 +94,7 @@ 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,6 +224,7 @@ 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,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
}
if a.packageInstalled("dms-shell-bin") {
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
}
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -540,7 +536,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
var dmsShell []string
for _, pkg := range packages {
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
if pkg == "dms-shell-git" {
dmsShell = append(dmsShell, pkg)
} else {
isDep := false
@@ -621,7 +617,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
}
}
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
if pkg == "dms-shell-git" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{
"depends = quickshell",
@@ -644,15 +640,7 @@ 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),
@@ -739,42 +727,9 @@ 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
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
}
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

@@ -31,6 +31,7 @@ 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"
@@ -72,6 +73,7 @@ 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
@@ -394,6 +396,18 @@ 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 {
@@ -1325,6 +1339,9 @@ func cleanupManagers() {
if themeModeManager != nil {
themeModeManager.Close()
}
if trayRecoveryManager != nil {
trayRecoveryManager.Close()
}
if wlContext != nil {
wlContext.Close()
}
@@ -1610,6 +1627,18 @@ func Start(printDocs bool) error {
}()
}
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
if err := InitializeTrayRecoveryManager(); err != nil {
log.Warnf("TrayRecovery manager unavailable: %v", err)
} else {
trayRecoveryManager.WatchLoginctl(loginctlManager)
}
}()
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient

View File

@@ -0,0 +1,93 @@
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

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

View File

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

View File

@@ -124,6 +124,8 @@ Singleton {
property string vpnLastConnected: ""
property string lastPlayerIdentity: ""
property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []

View File

@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton {
id: root
readonly property int settingsConfigVersion: 11
readonly property int settingsConfigVersion: 5
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -186,7 +186,6 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
@@ -199,33 +198,6 @@ Singleton {
property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false
property bool frameEnabled: false
onFrameEnabledChanged: saveSettings()
property real frameThickness: 16
onFrameThicknessChanged: saveSettings()
property real frameRounding: 23
onFrameRoundingChanged: saveSettings()
property string frameColor: ""
onFrameColorChanged: saveSettings()
property real frameOpacity: 1.0
onFrameOpacityChanged: saveSettings()
property var frameScreenPreferences: ["all"]
onFrameScreenPreferencesChanged: saveSettings()
property real frameBarSize: 40
onFrameBarSizeChanged: saveSettings()
property bool frameShowOnOverview: false
onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings()
readonly property color effectiveFrameColor: {
const fc = frameColor;
if (!fc || fc === "default") return Theme.surfaceContainer;
if (fc === "primary") return Theme.primary;
if (fc === "surface") return Theme.surface;
return fc;
}
property bool showLauncherButton: true
property bool showWorkspaceSwitcher: true
property bool showFocusedWindow: true
@@ -329,6 +301,7 @@ Singleton {
property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true
property bool mediaAdaptiveWidthEnabled: true
property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5
@@ -462,6 +435,7 @@ 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
@@ -1966,66 +1940,6 @@ Singleton {
return filtered;
}
function getFrameFilteredScreens() {
var prefs = frameScreenPreferences || ["all"];
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
return Quickshell.screens;
}
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
}
function getActiveBarEdgeForScreen(screen) {
if (!screen) return "";
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: return "top";
case SettingsData.Position.Bottom: return "bottom";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.Right: return "right";
}
}
return "";
}
function getActiveBarEdgesForScreen(screen) {
if (!screen) return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top: edges.push("top"); break;
case SettingsData.Position.Bottom: edges.push("bottom"); break;
case SettingsData.Position.Left: edges.push("left"); break;
case SettingsData.Position.Right: edges.push("right"); break;
}
}
return edges;
}
function getActiveBarThicknessForScreen(screen) {
if (frameEnabled) return frameBarSize;
if (!screen) return frameThickness;
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled) continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
const innerPadding = bc.innerPadding ?? 4;
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = bc.spacing ?? 4;
const bottomGap = bc.bottomGap ?? 0;
return barT + spacing + bottomGap;
}
return frameThickness;
}
function sendTestNotifications() {
NotificationService.dismissAllPopups();
sendTestNotification(0);

View File

@@ -75,6 +75,8 @@ var SPEC = {
vpnLastConnected: { def: "" },
lastPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] },

View File

@@ -140,6 +140,7 @@ var SPEC = {
workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true },
scrollTitleEnabled: { def: true },
mediaAdaptiveWidthEnabled: { def: true },
audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 },
@@ -242,6 +243,7 @@ var SPEC = {
soundsEnabled: { def: true },
useSystemSoundTheme: { def: false },
soundLogin: { def: false },
soundNewNotification: { def: true },
soundVolumeChanged: { def: true },
soundPluggedIn: { def: true },
@@ -547,17 +549,7 @@ var SPEC = {
clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] },
frameEnabled: { def: false },
frameThickness: { def: 16 },
frameRounding: { def: 23 },
frameColor: { def: "" },
frameOpacity: { def: 1.0 },
frameScreenPreferences: { def: ["all"] },
frameBarSize: { def: 40 },
frameShowOnOverview: { def: false },
frameBlurEnabled: { def: true }
launcherPluginOrder: { def: [] }
};
function getValidKeys() {

View File

@@ -248,10 +248,6 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 6;
}
if (currentVersion < 11) {
settings.configVersion = 11;
}
return settings;
}

View File

@@ -21,7 +21,6 @@ import qs.Modules.OSD
import qs.Modules.ProcessList
import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts
import qs.Modules.Frame
import qs.Modules.WorkspaceOverlays
import qs.Services
@@ -177,8 +176,6 @@ Item {
}
}
Frame {}
Repeater {
id: dankBarRepeater
model: ScriptModel {
@@ -224,10 +221,22 @@ Item {
}
}
Timer {
id: loginSoundTimer
// Half a second delay before playing login sound, otherwise the sound may be cut off
// 50 is the minimum that seems to work, but 500 is safer
interval: 500
repeat: false
onTriggered: {
AudioService.playLoginSoundIfApplicable();
}
}
Component.onCompleted: {
dockRecreateDebounce.start();
// Force PolkitService singleton to initialize
PolkitService.polkitAvailable;
loginSoundTimer.start();
}
Loader {

View File

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

View File

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

View File

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

View File

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

View File

@@ -518,20 +518,5 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: frameLoader
anchors.fill: parent
active: root.currentIndex === 33
visible: active
focus: active
sourceComponent: FrameTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}

View File

@@ -120,12 +120,6 @@ Rectangle {
"text": I18n.tr("Widgets"),
"icon": "widgets",
"tabIndex": 22
},
{
"id": "frame",
"text": I18n.tr("Frame"),
"icon": "frame_source",
"tabIndex": 33
}
]
},

View File

@@ -10,8 +10,6 @@ Item {
required property var axis
required property var barConfig
visible: !SettingsData.frameEnabled
anchors.fill: parent
anchors.left: parent.left
@@ -39,8 +37,6 @@ Item {
}
property real rt: {
if (SettingsData.frameEnabled)
return SettingsData.frameRounding;
if (barConfig?.squareCorners ?? false)
return 0;
if (barWindow.hasMaximizedToplevel)
@@ -259,12 +255,11 @@ Item {
h = h - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${crE} 0`;
d += ` L ${w - crE} 0`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
let d = `M ${cr} 0`;
d += ` L ${w - cr} 0`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
if (r > 0) {
d += ` L ${w} ${h + r}`;
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
@@ -278,9 +273,9 @@ Item {
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
}
d += ` L 0 ${crE}`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
d += ` L 0 ${cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
d += " Z";
return d;
}
@@ -290,12 +285,11 @@ Item {
h = h - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${crE} ${fullH}`;
d += ` L ${w - crE} ${fullH}`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
let d = `M ${cr} ${fullH}`;
d += ` L ${w - cr} ${fullH}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
if (r > 0) {
d += ` L ${w} 0`;
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
@@ -309,9 +303,9 @@ Item {
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
}
d += ` L 0 ${fullH - crE}`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
d += ` L 0 ${fullH - cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
d += " Z";
return d;
}
@@ -320,12 +314,11 @@ Item {
w = w - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M 0 ${crE}`;
d += ` L 0 ${h - crE}`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
let d = `M 0 ${cr}`;
d += ` L 0 ${h - cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
if (r > 0) {
d += ` L ${w + r} ${h}`;
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
@@ -339,9 +332,9 @@ Item {
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
}
d += ` L ${crE} 0`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
d += ` L ${cr} 0`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
d += " Z";
return d;
}
@@ -351,12 +344,11 @@ Item {
w = w - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
let d = `M ${fullW} ${crE}`;
d += ` L ${fullW} ${h - crE}`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
let d = `M ${fullW} ${cr}`;
d += ` L ${fullW} ${h - cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
if (r > 0) {
d += ` L 0 ${h}`;
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
@@ -370,9 +362,9 @@ Item {
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
}
d += ` L ${fullW - crE} 0`;
if (crE > 0)
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
d += ` L ${fullW - cr} 0`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
d += " Z";
return d;
}

View File

@@ -23,31 +23,6 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
readonly property real _frameLeftInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentLeftBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameRightInset: {
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
return barWindow.hasAdjacentRightBar
? SettingsData.frameBarSize
: 0
}
readonly property real _frameTopInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentTopBar
? SettingsData.frameThickness
: 0
}
readonly property real _frameBottomInset: {
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
return barWindow.hasAdjacentBottomBar
? SettingsData.frameThickness
: 0
}
property alias hLeftSection: hLeftSection
property alias hCenterSection: hCenterSection
property alias hRightSection: hRightSection
@@ -56,14 +31,10 @@ Item {
property alias vRightSection: vRightSection
anchors.fill: parent
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
anchors.topMargin: (barWindow.isVertical
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameTopInset
anchors.bottomMargin: (barWindow.isVertical
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
: 0) + _frameBottomInset
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
clip: false
property int componentMapRevision: 0
@@ -998,6 +969,7 @@ Item {
axis: barWindow.axis
barSpacing: barConfig?.spacing ?? 4
barConfig: topBarContent.barConfig
widgetData: parent.widgetData
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
isAtBottom: barWindow.axis?.edge === "bottom"
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
@@ -1466,12 +1438,21 @@ Item {
parentScreen: barWindow.screen
onClicked: {
systemUpdateLoader.active = true;
if (!systemUpdateLoader.item)
return;
const popout = systemUpdateLoader.item;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
if (popout.setBarContext) {
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
systemUpdateLoader.item?.toggle();
if (popout.setTriggerPosition) {
const globalPos = visualContent.mapToItem(null, 0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
}
}
}

View File

@@ -133,11 +133,6 @@ PanelWindow {
teardown();
if (!BlurService.enabled || !BlurService.available)
return;
// In frame mode, FrameWindow owns the blur region for the entire screen edge
// (including the bar area). The bar must not set its own competing blur region
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
if (SettingsData.frameEnabled)
return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency;
@@ -192,11 +187,6 @@ PanelWindow {
}
}
Connections {
target: SettingsData
function onFrameEnabledChanged() { barBlur.rebuild(); }
}
Connections {
target: topBarSlide
function onXChanged() {
@@ -248,9 +238,7 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property color _bgColor: SettingsData.frameEnabled
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -396,7 +384,7 @@ PanelWindow {
shouldHideForWindows = filtered.length > 0;
}
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
Behavior on effectiveSpacing {
enabled: barWindow.visible
@@ -407,12 +395,7 @@ PanelWindow {
}
readonly property int notificationCount: NotificationService.notifications.length
readonly property real effectiveBarThickness: SettingsData.frameEnabled
? SettingsData.frameBarSize
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
? SettingsData.frameShowOnOverview
: (barConfig?.openOnOverview ?? false)
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
readonly property bool hasAdjacentTopBar: {
@@ -668,7 +651,7 @@ PanelWindow {
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
@@ -809,7 +792,7 @@ PanelWindow {
}
property bool reveal: {
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
if (inOverviewWithShow)
return true;
@@ -906,7 +889,7 @@ PanelWindow {
top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined
}
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Modules.Plugins
import qs.Services
@@ -10,6 +11,8 @@ BasePill {
property bool batteryPopupVisible: false
property var popoutTarget: null
property real touchpadAccumulator: 0
readonly property int barPosition: {
switch (axis?.edge) {
case "top":
@@ -119,5 +122,44 @@ 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: "88%"
text: "100%"
}
StyledTextMetrics {

View File

@@ -17,7 +17,7 @@ BasePill {
property int availableWidth: 400
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property Toplevel activeWindow: null
property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse
property bool isAutoHideBar: false
@@ -38,10 +38,44 @@ 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

@@ -19,7 +19,8 @@ BasePill {
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property bool compactMode: false
property var widgetData: null
readonly property int textWidth: {
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
readonly property int maxTextWidth: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
switch (size) {
case 0:
@@ -36,10 +37,7 @@ BasePill {
if (isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2;
}
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
return 0;
}
readonly property int currentContentHeight: {
if (!isVerticalOrientation) {
@@ -99,7 +97,7 @@ BasePill {
if (isMouseWheelY) {
if (deltaY > 0) {
activePlayer.previous();
MprisController.previousOrRewind();
} else {
activePlayer.next();
}
@@ -107,7 +105,7 @@ BasePill {
scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
if (scrollAccumulatorY > 0) {
activePlayer.previous();
MprisController.previousOrRewind();
} else {
activePlayer.next();
}
@@ -119,7 +117,28 @@ BasePill {
content: Component {
Item {
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
id: contentRoot
readonly property real measuredTextWidth: {
if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
return 0;
// Preserve the fixed-width text slot even if metadata is briefly empty.
if (!root.adaptiveWidthEnabled)
return root.maxTextWidth;
if (textContainer.displayText.length === 0)
return 0;
const rawWidth = mediaText.contentWidth;
if (!isFinite(rawWidth) || rawWidth <= 0)
return 0;
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
}
readonly property int horizontalContentWidth: {
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
}
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
opacity: root.playerAvailable ? 1 : 0
@@ -132,8 +151,9 @@ BasePill {
Behavior on implicitWidth {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
duration: Theme.mediumDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
@@ -214,7 +234,7 @@ BasePill {
if (mouse.button === Qt.LeftButton) {
activePlayer.togglePlaying();
} else if (mouse.button === Qt.MiddleButton) {
activePlayer.previous();
MprisController.previousOrRewind();
} else if (mouse.button === Qt.RightButton) {
activePlayer.next();
}
@@ -269,7 +289,7 @@ BasePill {
}
anchors.verticalCenter: parent.verticalCenter
width: textWidth
width: contentRoot.measuredTextWidth
height: root.widgetThickness
visible: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
@@ -278,50 +298,95 @@ BasePill {
clip: true
color: "transparent"
StyledText {
id: mediaText
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
property real scrollOffset: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: needsScrolling ? -scrollOffset : 0
onTextChanged: {
scrollOffset = 0;
scrollAnimation.restart();
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
SequentialAnimation {
id: scrollAnimation
running: mediaText.needsScrolling && textContainer.visible
loops: Animation.Infinite
Item {
id: textClip
anchors.fill: parent
clip: true
PauseAnimation {
duration: 2000
StyledText {
id: mediaText
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
property real scrollOffset: 0
property real textShift: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: (needsScrolling ? -scrollOffset : 0) + textShift
opacity: 1
onTextChanged: {
scrollOffset = 0;
textShift = 0;
scrollAnimation.restart();
textChangeAnimation.restart();
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
SequentialAnimation {
id: scrollAnimation
running: mediaText.needsScrolling && textContainer.visible
loops: Animation.Infinite
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
}
PauseAnimation {
duration: 2000
}
SequentialAnimation {
id: textChangeAnimation
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
ParallelAnimation {
NumberAnimation {
target: mediaText
property: "opacity"
from: 0.7
to: 1
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
NumberAnimation {
target: mediaText
property: "textShift"
from: 4
to: 0
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
}
}
@@ -370,11 +435,7 @@ BasePill {
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.previous();
}
}
onClicked: MprisController.previousOrRewind()
}
}

View File

@@ -16,8 +16,11 @@ BasePill {
enableCursor: false
property var parentWindow: null
property var widgetData: null
property string section: "right"
property bool isAtBottom: false
property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -40,6 +43,76 @@ BasePill {
return `${id}::${tooltipTitle}`;
}
function trayIconSourceFor(trayItem) {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
function activateInlineTrayItem(trayItem, anchorItem) {
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
if (!trayItem) {
return;
}
if (!trayItem.hasMenu) {
const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function toggleIconName() {
const edge = root.axis?.edge;
if (root.useOverflowPopup) {
switch (edge) {
case "left":
return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
case "right":
return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
case "bottom":
return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
case "top":
return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
}
if (edge === "left" || edge === "right") {
return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
}
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
function callContextMenuFallback(trayItemId, globalX, globalY) {
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
@@ -78,6 +151,13 @@ BasePill {
item: item
}))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
key: getTrayItemKey(item),
item: item
}))
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
@@ -103,6 +183,7 @@ BasePill {
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0
@@ -198,10 +279,11 @@ BasePill {
id: rowComp
Row {
spacing: 0
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
Repeater {
model: ScriptModel {
values: root.mainBarItems
values: root.displayedMainBarItems
objectProp: "key"
}
@@ -209,29 +291,7 @@ BasePill {
id: delegateRoot
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize
height: root.barThickness
@@ -371,7 +431,8 @@ BasePill {
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
@@ -380,8 +441,8 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
root.dropTargetIndex = root.draggedIndex;
}
}
if (!dragHandler.dragging)
@@ -391,7 +452,8 @@ BasePill {
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
@@ -407,7 +469,8 @@ BasePill {
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
@@ -429,7 +492,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: root.menuOpen ? "expand_less" : "expand_more"
name: root.toggleIconName()
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
}
@@ -451,6 +514,301 @@ BasePill {
}
}
}
Repeater {
model: ScriptModel {
values: root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
}
}
Component {
id: inlineExpandedTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
visible: width > 0 || height > 0
Behavior on width {
enabled: !root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: inlineVisualContent
width: root.trayItemSize
height: root.trayItemSize
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
radius: Theme.cornerRadius
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
opacity: root.inlineExpanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
IconImage {
id: inlineIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !inlineIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: inlineItemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: inlineTrayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
enabled: root.inlineExpanded
onPressed: mouse => {
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
inlineItemRipple.trigger(pos.x, pos.y);
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
root.activateInlineTrayItem(trayItem, inlineVisualContent);
return;
}
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
}
}
}
}
Component {
id: verticalMainTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = root.draggedIndex;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
}
}
}
}
@@ -459,219 +817,23 @@ BasePill {
Column {
spacing: 0
// Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
// When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
// Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
Repeater {
model: ScriptModel {
values: root.mainBarItems
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
objectProp: "key"
}
delegate: verticalMainTrayItemDelegate
}
delegate: Item {
id: delegateRoot
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: delegateRoot.shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!delegateRoot.trayItem)
return;
if (!delegateRoot.trayItem.onlyMenu) {
delegateRoot.trayItem.activate();
return;
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
if (!delegateRoot.trayItem?.hasMenu) {
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
Item {
@@ -689,14 +851,7 @@ BasePill {
DankIcon {
anchors.centerIn: parent
name: {
const edge = root.axis?.edge;
if (edge === "left") {
return root.menuOpen ? "chevron_left" : "chevron_right";
} else {
return root.menuOpen ? "chevron_right" : "chevron_left";
}
}
name: root.toggleIconName()
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
}
@@ -718,6 +873,22 @@ BasePill {
}
}
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? root.displayedMainBarItems : []
objectProp: "key"
}
delegate: verticalMainTrayItemDelegate
}
}
}
@@ -733,7 +904,7 @@ BasePill {
blurRadius: Theme.cornerRadius
}
visible: root.menuOpen
visible: root.useOverflowPopup && root.menuOpen
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
@@ -749,13 +920,14 @@ BasePill {
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
}
Connections {
target: PopoutManager
function onPopoutOpening() {
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
}
}
@@ -1021,30 +1193,7 @@ BasePill {
delegate: Rectangle {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem?.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize + 4
height: root.trayItemSize + 4
@@ -1313,7 +1462,8 @@ BasePill {
onVisibleChanged: {
if (visible) {
updatePosition();
root.menuOpen = false;
if (root.useOverflowPopup)
root.menuOpen = false;
PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null);
}

View File

@@ -100,7 +100,7 @@ DankPopout {
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause();
}
MprisController.activePlayer = player;
MprisController.setActivePlayer(player);
root.__hideDropdowns();
}
onDeviceSelected: device => {

View File

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

View File

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

View File

@@ -1,17 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
Variants {
id: root
model: Quickshell.screens
FrameInstance {
required property var modelData
screen: modelData
}
}

View File

@@ -1,59 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
Item {
id: root
anchors.fill: parent
required property real cutoutTopInset
required property real cutoutBottomInset
required property real cutoutLeftInset
required property real cutoutRightInset
required property real cutoutRadius
Rectangle {
id: borderRect
anchors.fill: parent
// Bake frameOpacity into the color alpha rather than using the `opacity` property.
// Qt Quick can skip layer.effect processing on items with opacity < 1 as an
// optimization, causing the MultiEffect inverted mask to stop working and the
// Rectangle to render as a plain square at low opacity values.
color: Qt.rgba(SettingsData.effectiveFrameColor.r,
SettingsData.effectiveFrameColor.g,
SettingsData.effectiveFrameColor.b,
SettingsData.frameOpacity)
layer.enabled: true
layer.effect: MultiEffect {
maskSource: cutoutMask
maskEnabled: true
maskInverted: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Item {
id: cutoutMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors {
fill: parent
topMargin: root.cutoutTopInset
bottomMargin: root.cutoutBottomInset
leftMargin: root.cutoutLeftInset
rightMargin: root.cutoutRightInset
}
radius: root.cutoutRadius
}
}
}

View File

@@ -1,87 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
Scope {
id: root
required property var screen
readonly property var barEdges: {
SettingsData.barConfigs; // force re-eval when bar configs change
return SettingsData.getActiveBarEdgesForScreen(screen);
}
// One thin invisible PanelWindow per edge.
// Skips any edge where a bar already provides its own exclusiveZone.
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
Loader {
active: root.screenEnabled && !root.barEdges.includes("top")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorTop: true
anchorLeft: true
anchorRight: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("bottom")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorBottom: true
anchorLeft: true
anchorRight: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("left")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorLeft: true
anchorTop: true
anchorBottom: true
}
}
Loader {
active: root.screenEnabled && !root.barEdges.includes("right")
sourceComponent: EdgeExclusion {
targetScreen: root.screen
anchorRight: true
anchorTop: true
anchorBottom: true
}
}
component EdgeExclusion: PanelWindow {
required property var targetScreen
screen: targetScreen
property bool anchorTop: false
property bool anchorBottom: false
property bool anchorLeft: false
property bool anchorRight: false
WlrLayershell.namespace: "dms:frame-exclusion"
WlrLayershell.layer: WlrLayer.Top
exclusiveZone: SettingsData.frameThickness
color: "transparent"
mask: Region {}
implicitWidth: 1
implicitHeight: 1
anchors {
top: anchorTop
bottom: anchorBottom
left: anchorLeft
right: anchorRight
}
}
}

View File

@@ -1,18 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Item {
id: root
required property var screen
FrameWindow {
targetScreen: root.screen
}
FrameExclusions {
screen: root.screen
}
}

View File

@@ -1,169 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
PanelWindow {
id: win
required property var targetScreen
screen: targetScreen
visible: true
WlrLayershell.namespace: "dms:frame"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
// No input — pass everything through to apps and bar
mask: Region {}
readonly property var barEdges: {
SettingsData.barConfigs;
return SettingsData.getActiveBarEdgesForScreen(win.screen);
}
readonly property real _dpr: CompositorService.getScreenScale(win.screen)
readonly property bool _frameActive: SettingsData.frameEnabled
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property int _windowRegionWidth: win._regionInt(win.width)
readonly property int _windowRegionHeight: win._regionInt(win.height)
function _regionInt(value) {
return Math.max(0, Math.round(Theme.px(value, win._dpr)));
}
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
readonly property int cutoutRadius: {
const requested = win._regionInt(SettingsData.frameRounding);
const maxRadius = Math.floor(Math.min(win.cutoutWidth, win.cutoutHeight) / 2);
return Math.max(0, Math.min(requested, maxRadius));
}
// Slightly expand the subtractive blur cutout at very low opacity levels
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
readonly property int _blurCutoutRight: Math.min(win._windowRegionWidth, win._windowRegionWidth - win.cutoutRightInset + win._blurCutoutCompensation)
readonly property int _blurCutoutBottom: Math.min(win._windowRegionHeight, win._windowRegionHeight - win.cutoutBottomInset + win._blurCutoutCompensation)
readonly property int _blurCutoutRadius: {
const requested = win.cutoutRadius + win._blurCutoutCompensation;
const maxRadius = Math.floor(Math.min(_blurCutout.width, _blurCutout.height) / 2);
return Math.max(0, Math.min(requested, maxRadius));
}
// Must stay visible so Region.item can resolve scene coordinates.
Item {
id: _blurCutout
x: win._blurCutoutLeft
y: win._blurCutoutTop
width: Math.max(0, win._blurCutoutRight - win._blurCutoutLeft)
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
}
property var _frameBlurRegion: null
function _buildBlur() {
_teardownBlur();
// Follow the global blur toggle
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
return;
try {
const region = Qt.createQmlObject(
'import QtQuick; import Quickshell; Region {' +
' property Item cutoutItem;' +
' property int cutoutRadius: 0;' +
' Region {' +
' item: cutoutItem;' +
' intersection: Intersection.Subtract;' +
' radius: cutoutRadius;' +
' }' +
'}',
win, "FrameBlurRegion");
region.x = Qt.binding(() => 0);
region.y = Qt.binding(() => 0);
region.width = Qt.binding(() => win._windowRegionWidth);
region.height = Qt.binding(() => win._windowRegionHeight);
region.cutoutItem = _blurCutout;
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
win.BackgroundEffect.blurRegion = region;
win._frameBlurRegion = region;
} catch (e) {
console.warn("FrameWindow: Failed to create blur region:", e);
}
}
function _teardownBlur() {
if (!win._frameBlurRegion)
return;
try {
win.BackgroundEffect.blurRegion = null;
} catch (e) {}
win._frameBlurRegion.destroy();
win._frameBlurRegion = null;
}
Timer {
id: _blurRebuildTimer
interval: 1
onTriggered: win._buildBlur()
}
Connections {
target: SettingsData
function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
}
Connections {
target: BlurService
function onEnabledChanged() { _blurRebuildTimer.restart(); }
}
onVisibleChanged: {
if (visible) {
win._frameBlurRegion = null;
_blurRebuildTimer.restart();
} else {
_teardownBlur();
}
}
Component.onCompleted: Qt.callLater(() => win._buildBlur())
Component.onDestruction: win._teardownBlur()
FrameBorder {
anchors.fill: parent
visible: win._frameActive
cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
}

View File

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

View File

@@ -693,8 +693,6 @@ Item {
SettingsToggleRow {
visible: CompositorService.isNiri
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview")
checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => {
@@ -800,42 +798,11 @@ Item {
}
}
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: frameNote.implicitHeight + Theme.spacingS * 2
Row {
id: frameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Spacing and size are managed by Frame mode")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard {
iconName: "space_bar"
title: I18n.tr("Spacing")
settingKey: "barSpacing"
visible: selectedBarConfig?.enabled
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
SettingsSliderRow {
id: edgeSpacingSlider
@@ -1036,8 +1003,6 @@ Item {
SettingsSliderRow {
id: barTransparencySlider
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Bar Transparency")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
@@ -1079,35 +1044,6 @@ Item {
restoreMode: Binding.RestoreBinding
}
}
Item {
visible: SettingsData.frameEnabled
width: parent.width
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
Row {
id: transparencyFrameNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
}
SettingsCard {
@@ -1351,8 +1287,6 @@ Item {
SettingsToggleRow {
text: I18n.tr("Square Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
squareCorners: checked
@@ -1400,8 +1334,6 @@ Item {
SettingsToggleRow {
text: I18n.tr("Goth Corners")
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
gothCornersEnabled: checked

View File

@@ -1,295 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
// ── Enable Frame ──────────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "frame_source"
title: I18n.tr("Frame")
settingKey: "frameEnabled"
SettingsToggleRow {
settingKey: "frameEnable"
tags: ["frame", "border", "outline", "display"]
text: I18n.tr("Enable Frame")
description: I18n.tr("Draw a connected picture-frame border around the entire display")
checked: SettingsData.frameEnabled
onToggled: checked => SettingsData.set("frameEnabled", checked)
}
}
// ── Border ────────────────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "border_outer"
title: I18n.tr("Border")
settingKey: "frameBorder"
collapsible: true
visible: SettingsData.frameEnabled
SettingsSliderRow {
id: roundingSlider
settingKey: "frameRounding"
tags: ["frame", "border", "rounding", "radius", "corner"]
text: I18n.tr("Border Radius")
unit: "px"
minimum: 0
maximum: 100
step: 1
defaultValue: 23
value: SettingsData.frameRounding
onSliderDragFinished: v => SettingsData.set("frameRounding", v)
Binding {
target: roundingSlider
property: "value"
value: SettingsData.frameRounding
}
}
SettingsSliderRow {
id: thicknessSlider
settingKey: "frameThickness"
tags: ["frame", "border", "thickness", "size", "width"]
text: I18n.tr("Border Width")
unit: "px"
minimum: 2
maximum: 100
step: 1
defaultValue: 16
value: SettingsData.frameThickness
onSliderDragFinished: v => SettingsData.set("frameThickness", v)
Binding {
target: thicknessSlider
property: "value"
value: SettingsData.frameThickness
}
}
SettingsSliderRow {
id: barThicknessSlider
settingKey: "frameBarSize"
tags: ["frame", "bar", "thickness", "size", "height", "width"]
text: I18n.tr("Size")
description: I18n.tr("Height of horizontal bars / width of vertical bars in frame mode")
unit: "px"
minimum: 24
maximum: 100
step: 1
defaultValue: 40
value: SettingsData.frameBarSize
onSliderDragFinished: v => SettingsData.set("frameBarSize", v)
Binding {
target: barThicknessSlider
property: "value"
value: SettingsData.frameBarSize
}
}
SettingsSliderRow {
id: opacitySlider
settingKey: "frameOpacity"
tags: ["frame", "border", "opacity", "transparency"]
text: I18n.tr("Frame Opacity")
unit: "%"
minimum: 0
maximum: 100
defaultValue: 100
value: SettingsData.frameOpacity * 100
onSliderDragFinished: v => SettingsData.set("frameOpacity", v / 100)
Binding {
target: opacitySlider
property: "value"
value: SettingsData.frameOpacity * 100
}
}
SettingsToggleRow {
id: frameBlurToggle
settingKey: "frameBlurEnabled"
tags: ["frame", "blur", "background", "glass", "transparency", "frosted"]
text: I18n.tr("Frame Blur")
description: !BlurService.available
? I18n.tr("Requires a newer version of Quickshell")
: I18n.tr("Apply compositor blur behind the frame border")
checked: SettingsData.frameBlurEnabled
onToggled: checked => SettingsData.set("frameBlurEnabled", checked)
enabled: BlurService.available && SettingsData.blurEnabled
opacity: enabled ? 1.0 : 0.5
visible: BlurService.available
}
Item {
visible: BlurService.available && !SettingsData.blurEnabled
width: parent.width
height: blurToggleNote.height + Theme.spacingM * 2
Row {
id: blurToggleNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "blur_on"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Frame Blur is controlled by Background Blur in Theme & Colors")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
// Color mode buttons
SettingsButtonGroupRow {
settingKey: "frameColor"
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
text: I18n.tr("Border color")
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
currentIndex: {
const fc = SettingsData.frameColor;
if (!fc || fc === "default") return 0;
if (fc === "primary") return 1;
if (fc === "surface") return 2;
return 3;
}
onSelectionChanged: (index, selected) => {
if (!selected) return;
switch (index) {
case 0: SettingsData.set("frameColor", ""); break;
case 1: SettingsData.set("frameColor", "primary"); break;
case 2: SettingsData.set("frameColor", "surface"); break;
case 3:
const cur = SettingsData.frameColor;
const isPreset = !cur || cur === "primary" || cur === "surface";
if (isPreset) SettingsData.set("frameColor", "#2a2a2a");
break;
}
}
}
// Custom color swatch — only visible when a hex color is stored (Custom mode)
Item {
visible: {
const fc = SettingsData.frameColor;
return !!(fc && fc !== "primary" && fc !== "surface");
}
width: parent.width
height: customColorRow.height + Theme.spacingM * 2
Row {
id: customColorRow
width: parent.width - Theme.spacingM * 2
x: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Custom color")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Rectangle {
id: colorSwatch
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
radius: 16
color: SettingsData.effectiveFrameColor
border.color: Theme.outline
border.width: 1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
PopoutService.colorPickerModal.selectedColor = SettingsData.effectiveFrameColor;
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Frame Border Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
SettingsData.set("frameColor", color.toString());
};
PopoutService.colorPickerModal.show();
}
}
}
}
}
}
// ── Bar Integration ───────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "toolbar"
title: I18n.tr("Bar Integration")
settingKey: "frameBarIntegration"
collapsible: true
expanded: false
visible: SettingsData.frameEnabled
SettingsToggleRow {
visible: CompositorService.isNiri
settingKey: "frameShowOnOverview"
tags: ["frame", "overview", "show", "hide", "niri"]
text: I18n.tr("Show on Overview")
description: I18n.tr("Show the bar and frame during Niri overview mode")
checked: SettingsData.frameShowOnOverview
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
}
}
// ── Display Assignment ────────────────────────────────────────────
SettingsCard {
width: parent.width
iconName: "monitor"
title: I18n.tr("Display Assignment")
settingKey: "frameDisplays"
collapsible: true
expanded: false
visible: SettingsData.frameEnabled
SettingsDisplayPicker {
displayPreferences: SettingsData.frameScreenPreferences
onPreferencesChanged: prefs => SettingsData.set("frameScreenPreferences", prefs)
}
}
}
}
}

View File

@@ -46,6 +46,13 @@ Item {
onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked)
}
SettingsToggleRow {
text: I18n.tr("Adaptive Media Width")
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
checked: SettingsData.mediaAdaptiveWidthEnabled
onToggled: checked => SettingsData.set("mediaAdaptiveWidthEnabled", checked)
}
SettingsDropdownRow {
property var scrollOptsInternal: ["volume", "song", "nothing"]
property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")]

View File

@@ -91,6 +91,16 @@ 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

@@ -83,6 +83,7 @@ Item {
description: modelData.width + "×" + modelData.height
checked: localChecked
onToggled: isChecked => {
localChecked = isChecked;
var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
if (!Array.isArray(prefs) || prefs.includes("all"))
prefs = [];
@@ -93,11 +94,6 @@ Item {
model: modelData.model || ""
});
}
if (prefs.length === 0) {
localChecked = true;
return;
}
localChecked = isChecked;
root.preferencesChanged(prefs);
}
}

View File

@@ -430,7 +430,7 @@ Item {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -712,6 +712,8 @@ Item {
item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps;
if (widget.barShowOverflowBadge !== undefined)
item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
}
widgets.push(item);
});

View File

@@ -39,7 +39,7 @@ Column {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -437,7 +437,7 @@ Column {
Row {
spacing: Theme.spacingXS
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock"
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
DankActionButton {
id: compactModeButton
@@ -543,6 +543,39 @@ Column {
}
}
DankActionButton {
id: trayMenuButton
buttonSize: 32
visible: modelData.id === "systemTray"
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
trayContextMenu.widgetData = modelData;
trayContextMenu.sectionId = root.sectionId;
trayContextMenu.widgetIndex = index;
var buttonPos = trayMenuButton.mapToItem(root, 0, 0);
var popupWidth = trayContextMenu.width;
var popupHeight = trayContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0)
xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS;
var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
trayContextMenu.x = xPos;
trayContextMenu.y = yPos;
trayContextMenu.open();
}
}
Rectangle {
id: compactModeTooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
@@ -931,6 +964,88 @@ Column {
}
}
Popup {
id: trayContextMenu
property var widgetData: null
property string sectionId: ""
property int widgetIndex: -1
readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData
width: 220
height: contentColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
}
contentItem: Item {
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "arrow_selector_tool"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Use Inline Expansion")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayOverflowToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false
}
MouseArea {
id: trayOverflowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue);
}
}
}
}
}
}
Popup {
id: diskUsageContextMenu

View File

@@ -26,6 +26,7 @@ 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
@@ -67,6 +68,16 @@ 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));
@@ -395,7 +406,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; do
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
found=0
case "$event_key" in
@@ -457,7 +468,8 @@ 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"
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
};
const specialConditions = {
@@ -551,6 +563,10 @@ EOFCONFIG
criticalNotificationSound.destroy();
criticalNotificationSound = null;
}
if (loginSound) {
loginSound.destroy();
loginSound = null;
}
}
function createSoundPlayers() {
@@ -622,6 +638,19 @@ 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);
}
@@ -661,6 +690,31 @@ 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

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

View File

@@ -4,10 +4,75 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import qs.Common
Singleton {
id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
property MprisPlayer activePlayer: null
onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function _resolveActivePlayer(): void {
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) {
activePlayer = playing;
_persistIdentity(playing.identity);
return;
}
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
const savedId = SessionData.lastPlayerIdentity;
if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId);
if (match) {
activePlayer = match;
return;
}
}
activePlayer = availablePlayers.find(p => p.canControl && p.canPlay) ?? null;
if (activePlayer)
_persistIdentity(activePlayer.identity);
}
function setActivePlayer(player: MprisPlayer): void {
activePlayer = player;
if (player)
_persistIdentity(player.identity);
}
function _persistIdentity(identity: string): void {
if (identity && SessionData.lastPlayerIdentity !== identity)
SessionData.set("lastPlayerIdentity", identity);
}
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

@@ -6,6 +6,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Services
Singleton {
id: root
@@ -58,6 +59,10 @@ Singleton {
}
readonly property bool screensharingActive: {
if (CompositorService.isNiri && NiriService.hasActiveCast) {
return true
}
if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false
}
@@ -74,6 +79,12 @@ Singleton {
}
}
if (node.properties && node.properties["media.class"] === "Stream/Output/Video") {
if (looksLikeScreencast(node)) {
return true
}
}
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
const mediaName = (node.properties["media.name"] || "").toLowerCase()
const appName = (node.properties["application.name"] || "").toLowerCase()
@@ -110,8 +121,9 @@ Singleton {
}
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const nodeName = (node.name || "").toLowerCase()
const combined = appName + " " + nodeName
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
const combined = appName + " " + nodeName + " " + mediaName
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
}
function getMicrophoneStatus() {

View File

@@ -231,7 +231,10 @@ Singleton {
return;
isChecking = true;
hasError = false;
if (updChecker.length > 0) {
if (pkgManager === "paru" || pkgManager === "yay") {
const repoCmd = updChecker.length > 0 ? updChecker : `${pkgManager} -Qu`;
updateChecker.command = ["sh", "-c", `(${repoCmd} 2>/dev/null; ${pkgManager} -Qua 2>/dev/null) || true`];
} else if (updChecker.length > 0) {
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
} else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);

View File

@@ -8,13 +8,122 @@ Item {
id: root
property MprisPlayer activePlayer
property real value: {
if (!activePlayer || activePlayer.length <= 0) return 0
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length)
const calculatedRatio = pos / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
property real seekPreviewRatio: -1
readonly property real playerValue: {
if (!activePlayer || activePlayer.length <= 0)
return 0;
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio));
}
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
property bool isSeeking: false
property bool isDraggingSeek: false
property real committedSeekRatio: -1
property int previewSettleChecksRemaining: 0
property real dragThreshold: 4
property int holdIndicatorDelay: 180
function clampRatio(ratio) {
return Math.max(0, Math.min(1, ratio));
}
function ratioForPosition(position) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
return clampRatio(position / activePlayer.length);
}
function positionForRatio(ratio) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
const rawPosition = clampRatio(ratio) * activePlayer.length;
return Math.min(rawPosition, activePlayer.length * 0.99);
}
function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || activePlayer.length <= 0 || width <= 0)
return;
seekPreviewRatio = clampRatio(mouseX / width);
}
function clearCommittedSeekPreview() {
previewSettleTimer.stop();
committedSeekRatio = -1;
previewSettleChecksRemaining = 0;
if (!isSeeking)
seekPreviewRatio = -1;
}
function beginCommittedSeekPreview(position) {
seekPreviewRatio = ratioForPosition(position);
committedSeekRatio = seekPreviewRatio;
previewSettleChecksRemaining = 15;
previewSettleTimer.restart();
}
function handleSeekPressed(mouse, width, mouseArea, holdTimer) {
isSeeking = true;
isDraggingSeek = false;
mouseArea.pressX = mouse.x;
clearCommittedSeekPreview();
holdTimer.restart();
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekReleased(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
activePlayer.position = clamped;
mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped);
} else {
seekPreviewRatio = -1;
}
}
function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
isDraggingSeek = true;
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekCanceled(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
mouseArea.pendingSeekPosition = -1;
clearCommittedSeekPreview();
}
Timer {
id: previewSettleTimer
interval: 80
repeat: true
onTriggered: {
if (root.isSeeking || root.committedSeekRatio < 0) {
stop();
return;
}
const previewSettled = Math.abs(root.playerValue - root.committedSeekRatio) <= 0.0015;
if (previewSettled || root.previewSettleChecksRemaining <= 0) {
root.clearCommittedSeekPreview();
return;
}
root.previewSettleChecksRemaining -= 1;
}
}
implicitHeight: 20
@@ -29,58 +138,35 @@ Item {
M3WaveProgress {
value: root.value
actualValue: root.playerValue
showActualPlaybackState: root.isSeeking
actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
MouseArea {
id: waveMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: waveSeekDebounceTimer
interval: 150
id: waveHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
waveSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
}
}
}
@@ -93,6 +179,7 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
readonly property real midY: height / 2
Rectangle {
@@ -110,7 +197,22 @@ Item {
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
Behavior on width {
NumberAnimation {
duration: 80
}
}
}
Rectangle {
visible: root.isDraggingSeek
width: 2
height: Math.max(parent.lineWidth + 4, 10)
radius: width / 2
color: parent.actualProgressColor
x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
y: parent.midY - height / 2
z: 2
}
Rectangle {
@@ -122,59 +224,37 @@ Item {
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
y: parent.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
Behavior on x {
NumberAnimation {
duration: 80
}
}
}
MouseArea {
id: flatMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1
property real pressX: 0
Timer {
id: flatSeekDebounceTimer
interval: 150
id: flatHoldIndicatorTimer
interval: root.holdIndicatorDelay
repeat: false
onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
if (parent.pressed && root.isSeeking)
root.isDraggingSeek = true;
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
flatSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
}
}
}

View File

@@ -6,6 +6,8 @@ Item {
id: root
property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2
property real wavelength: 20
property real amp: 1.6
@@ -15,6 +17,7 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) {
@@ -22,7 +25,12 @@ Item {
}
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 {
NumberAnimation {
@@ -65,7 +73,9 @@ Item {
readonly property real startX: snap(root.lineWidth / 2)
readonly property real aaBias: (0.25 / root.dpr)
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
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
@@ -100,6 +110,37 @@ Item {
}
}
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)
@@ -107,7 +148,7 @@ Item {
radius: width / 2
color: root.fillColor
x: waveClip.startX - width / 2
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
y: waveY(waveClip.startX) - height / 2
visible: waveClip.endX > waveClip.startX
z: 2
}
@@ -119,10 +160,34 @@ Item {
radius: width / 2
color: root.fillColor
x: waveClip.endX - width / 2
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
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 {
@@ -141,6 +206,10 @@ Item {
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 {
@@ -148,8 +217,9 @@ Item {
onTriggered: {
if (root.isPlaying)
root.phase += 0.03 * frameTime * 60;
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
}
}

Binary file not shown.

Binary file not shown.

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 []
context_str = " | ".join(contexts) if contexts else term
comment = " | ".join(contexts) if contexts else ""
entry = {
"term": term,
"context": context_str,
"context": term,
"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,6 +2112,26 @@
],
"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",
@@ -2378,6 +2398,47 @@
],
"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",
@@ -2405,6 +2466,49 @@
],
"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",
@@ -4384,27 +4488,6 @@
],
"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",
@@ -5018,6 +5101,26 @@
],
"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",
@@ -6407,6 +6510,27 @@
"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,6 +1182,13 @@
"reference": "",
"comment": ""
},
{
"term": "Applying authentication changes…",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Apps",
"translation": "",
@@ -1378,6 +1385,34 @@
"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": "",
@@ -1518,6 +1553,13 @@
"reference": "",
"comment": ""
},
{
"term": "Autofill last remembered query when opened",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Automatic Color Mode",
"translation": "",
@@ -1672,6 +1714,13 @@
"reference": "",
"comment": ""
},
{
"term": "Background Blur",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Background Opacity",
"translation": "",
@@ -1679,6 +1728,13 @@
"reference": "",
"comment": ""
},
{
"term": "Background authentication sync failed. Trying terminal mode.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Background image",
"translation": "",
@@ -1875,6 +1931,20 @@
"reference": "",
"comment": ""
},
{
"term": "Blur Border Color",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Blur Border Opacity",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Blur Wallpaper Layer",
"translation": "",
@@ -1889,6 +1959,13 @@
"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": "",
@@ -1938,6 +2015,13 @@
"reference": "",
"comment": ""
},
{
"term": "Border color around blurred surfaces",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Border with BG",
"translation": "",
@@ -2289,7 +2373,7 @@
"comment": ""
},
{
"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.",
"term": "Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.",
"translation": "",
"context": "",
"reference": "",
@@ -3544,7 +3628,7 @@
{
"term": "Custom",
"translation": "",
"context": "shadow color option | theme category option",
"context": "blur border color | shadow color option | theme category option",
"reference": "",
"comment": ""
},
@@ -3717,7 +3801,7 @@
"comment": ""
},
{
"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.",
"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.",
"translation": "",
"context": "",
"reference": "",
@@ -4788,7 +4872,7 @@
"comment": ""
},
{
"term": "Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.",
"term": "Enable fingerprint or security key for DMS Greeter. Authentication changes apply automatically.",
"translation": "",
"context": "",
"reference": "",
@@ -4836,6 +4920,13 @@
"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": "",
@@ -4844,7 +4935,7 @@
"comment": ""
},
{
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.",
"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.",
"translation": "",
"context": "",
"reference": "",
@@ -4857,13 +4948,6 @@
"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": "",
@@ -5809,6 +5893,27 @@
"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": "",
@@ -7139,6 +7244,13 @@
"reference": "",
"comment": ""
},
{
"term": "Incorrect password - try again",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Indicator Style",
"translation": "",
@@ -7202,6 +7314,13 @@
"reference": "",
"comment": ""
},
{
"term": "Insert your security key...",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Install",
"translation": "",
@@ -7930,6 +8049,13 @@
"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": "",
@@ -8315,6 +8441,13 @@
"reference": "",
"comment": ""
},
{
"term": "Maximum fingerprint attempts reached. Please use password.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Maximum number of clipboard entries to keep",
"translation": "",
@@ -10131,7 +10264,7 @@
{
"term": "Outline",
"translation": "",
"context": "outline color",
"context": "blur border color | outline color",
"reference": "",
"comment": ""
},
@@ -11062,7 +11195,7 @@
{
"term": "Primary",
"translation": "",
"context": "button color option | color option | primary color | shadow color option | tile color option",
"context": "blur border color | button color option | color option | primary color | shadow color option | tile color option",
"reference": "",
"comment": ""
},
@@ -11479,6 +11612,13 @@
"reference": "",
"comment": ""
},
{
"term": "Remember Last Query",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Remember last session",
"translation": "",
@@ -11605,6 +11745,13 @@
"reference": "",
"comment": ""
},
{
"term": "Requires a newer version of Quickshell",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Requires night mode support",
"translation": "",
@@ -11850,20 +11997,6 @@
"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": "",
@@ -12259,7 +12392,7 @@
{
"term": "Secondary",
"translation": "",
"context": "button color option | color option | secondary color | tile color option",
"context": "blur border color | button color option | color option | secondary color | tile color option",
"reference": "",
"comment": ""
},
@@ -14055,6 +14188,13 @@
"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": "",
@@ -14062,6 +14202,13 @@
"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": "",
@@ -14076,6 +14223,13 @@
"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": "",
@@ -14128,7 +14282,7 @@
{
"term": "Text Color",
"translation": "",
"context": "shadow color option",
"context": "blur border color | shadow color option",
"reference": "",
"comment": ""
},
@@ -14489,6 +14643,13 @@
"reference": "",
"comment": ""
},
{
"term": "Too many attempts - locked out",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Too many failed attempts - account may be locked",
"translation": "",
@@ -14573,6 +14734,13 @@
"reference": "",
"comment": ""
},
{
"term": "Touch your security key...",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Transform",
"translation": "",
@@ -14678,20 +14846,6 @@
"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": "",