mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-07 04:52:06 -04:00
Compare commits
16 Commits
frame
...
3194fc3fbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3194fc3fbe | ||
|
|
3318864ece | ||
|
|
e224417593 | ||
|
|
3f7f6c5d2c | ||
|
|
0b88055742 | ||
|
|
2b0826e397 | ||
|
|
7db04c9660 | ||
|
|
14d1e1d985 | ||
|
|
903ab1e61d | ||
|
|
5982655539 | ||
|
|
1021a210cf | ||
|
|
e34edb15bb | ||
|
|
61ee5f4336 | ||
|
|
ce2a92ec27 | ||
|
|
66ce79b9bf | ||
|
|
30dd640314 |
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)$
|
||||
|
||||
@@ -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$"#
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
93
core/internal/server/trayrecovery/manager.go
Normal file
93
core/internal/server/trayrecovery/manager.go
Normal 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")
|
||||
}
|
||||
262
core/internal/server/trayrecovery/recovery.go
Normal file
262
core/internal/server/trayrecovery/recovery.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -434,6 +434,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
|
||||
|
||||
@@ -242,6 +242,7 @@ var SPEC = {
|
||||
|
||||
soundsEnabled: { def: true },
|
||||
useSystemSoundTheme: { def: false },
|
||||
soundLogin: { def: false },
|
||||
soundNewNotification: { def: true },
|
||||
soundVolumeChanged: { def: true },
|
||||
soundPluggedIn: { def: true },
|
||||
|
||||
@@ -221,10 +221,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: loginSoundTimer
|
||||
// Half a second delay before playing login sound, otherwise the sound may be cut off
|
||||
// 50 is the minimum that seems to work, but 500 is safer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
AudioService.playLoginSoundIfApplicable();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
dockRecreateDebounce.start();
|
||||
// Force PolkitService singleton to initialize
|
||||
PolkitService.polkitAvailable;
|
||||
loginSoundTimer.start();
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -369,9 +369,7 @@ Item {
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
||||
MprisController.activePlayer.previous();
|
||||
}
|
||||
MprisController.previousOrRewind();
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -99,7 +99,7 @@ BasePill {
|
||||
|
||||
if (isMouseWheelY) {
|
||||
if (deltaY > 0) {
|
||||
activePlayer.previous();
|
||||
MprisController.previousOrRewind();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -107,7 +107,7 @@ BasePill {
|
||||
scrollAccumulatorY += deltaY;
|
||||
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
||||
if (scrollAccumulatorY > 0) {
|
||||
activePlayer.previous();
|
||||
MprisController.previousOrRewind();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -214,7 +214,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();
|
||||
}
|
||||
@@ -370,11 +370,7 @@ BasePill {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,4 +10,20 @@ Singleton {
|
||||
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||
repeat: true
|
||||
onTriggered: root.activePlayer?.positionChanged()
|
||||
}
|
||||
|
||||
function previousOrRewind(): void {
|
||||
if (!activePlayer)
|
||||
return;
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek)
|
||||
activePlayer.position = 0;
|
||||
else if (activePlayer.canGoPrevious)
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
|
||||
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
Binary file not shown.
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
Reference in New Issue
Block a user