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

Compare commits

...

11 Commits

Author SHA1 Message Date
purian23 6cc574ea5b refactor: unify media control calls to MprisController sync 2026-07-15 14:15:30 -04:00
bbedward cdaedad969 media: throttle frame rate of media-related animations
port 1.5

related #2869
2026-07-15 12:57:25 -04:00
bbedward bf408f8d00 i18n: cut down terms and sync
port 1.5
2026-07-15 11:05:07 -04:00
purian23 b169fe0d77 feat: new PAM auth management & settings in greeter/lockscreen
- Introduce external management of greetd PAM
- New functionality to validate and apply custom PAM service paths in lockscreen

Port 1.5
2026-07-15 10:38:06 -04:00
bbedward 9b67cedaa1 battery: dont scale values and skew the actual battery values
fixes #2867

port 1.5
2026-07-15 10:20:59 -04:00
bbedward 2edf70a144 tailscale: fix inconsistency in styling of control center widget
port 1.5
2026-07-15 10:15:32 -04:00
bbedward 72a71cacba hyprland: scale overview displays relative to their actual scale
port 1.5
2026-07-15 10:10:33 -04:00
bbedward 0440e40a82 compositor: improve compositor detection
port 1.5
2026-07-15 09:45:25 -04:00
bbedward ec27d4643c qs: improve initial load of wallpaper, dock, and desktop plugins 2026-07-15 09:38:19 -04:00
bbedward f18d36f6c2 screenshot: add --json flag to capture metadata
fixes #2852

port 1.5
2026-07-15 09:17:55 -04:00
purian23 3254cc6a1e update release & changelog docs formatting
port 1.5
2026-07-15 00:57:26 -04:00
38 changed files with 1883 additions and 982 deletions
+100
View File
@@ -1,7 +1,10 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -56,10 +59,107 @@ var authResolveLockCmd = &cobra.Command{
}, },
} }
var authListServicesCmd = &cobra.Command{
Use: "list-services",
Short: "List candidate lock-screen PAM services available on this system",
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
Run: func(cmd *cobra.Command, args []string) {
asJSON, _ := cmd.Flags().GetBool("json")
services := sharedpam.ListLockscreenPamServices()
if asJSON {
payload := struct {
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
}{Services: services}
data, err := json.MarshalIndent(payload, "", " ")
if err != nil {
log.Fatalf("Error encoding services: %v", err)
}
fmt.Println(string(data))
return
}
if len(services) == 0 {
fmt.Println("No candidate lock-screen PAM services found.")
return
}
for _, s := range services {
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
}
},
}
var authValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a PAM service file for use as the DMS lock-screen password stack",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password stack. Exits 1 when the file is not usable.",
Run: func(cmd *cobra.Command, args []string) {
path, _ := cmd.Flags().GetString("path")
service, _ := cmd.Flags().GetString("service")
asJSON, _ := cmd.Flags().GetBool("json")
if (path == "") == (service == "") {
log.Fatalf("Error: exactly one of --path or --service is required")
}
var result sharedpam.LockscreenPamValidation
switch {
case service != "":
result = sharedpam.ValidateLockscreenPamService(service)
case !filepath.IsAbs(path):
result = sharedpam.LockscreenPamValidation{
Path: path,
MissingModules: []string{},
Warnings: []string{},
Errors: []string{"--path must be an absolute file path"},
}
default:
result = sharedpam.ValidateLockscreenPamPath(path)
}
if asJSON {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatalf("Error encoding validation: %v", err)
}
fmt.Println(string(data))
} else {
printLockscreenPamValidation(result)
}
if !result.Valid {
os.Exit(1)
}
},
}
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
fmt.Printf("Path: %s\n", result.Path)
fmt.Printf("Valid: %t\n", result.Valid)
fmt.Printf("Has auth: %t\n", result.HasAuth)
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
if len(result.MissingModules) > 0 {
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
}
for _, w := range result.Warnings {
fmt.Println("⚠ " + w)
}
for _, e := range result.Errors {
fmt.Println("✗ " + e)
}
}
func init() { func init() {
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts") authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)") authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path") authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
} }
func syncAuth(nonInteractive bool) error { func syncAuth(nonInteractive bool) error {
+69 -10
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -27,8 +28,19 @@ var (
ssNoConfirm bool ssNoConfirm bool
ssReset bool ssReset bool
ssStdout bool ssStdout bool
ssJSON bool
) )
type screenshotMetadata struct {
Status string `json:"status"`
Path string `json:"path,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Scale float64 `json:"scale,omitempty"`
Mime string `json:"mime,omitempty"`
Error string `json:"error,omitempty"`
}
var screenshotCmd = &cobra.Command{ var screenshotCmd = &cobra.Command{
Use: "screenshot", Use: "screenshot",
Short: "Capture screenshots", Short: "Capture screenshots",
@@ -59,7 +71,8 @@ Examples:
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`, dms screenshot -f jpg -q 85 # JPEG with quality 85
dms screenshot --json # Print capture metadata as JSON`,
} }
var ssRegionCmd = &cobra.Command{ var ssRegionCmd = &cobra.Command{
@@ -128,6 +141,7 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation") screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing") screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssRegionCmd)
screenshotCmd.AddCommand(ssFullCmd) screenshotCmd.AddCommand(ssFullCmd)
@@ -203,7 +217,36 @@ func setPopoutScreenshotMode(begin bool) {
_ = exec.Command("qs", cmdArgs...).Run() _ = exec.Command("qs", cmdArgs...).Run()
} }
func writeScreenshotJSON(meta screenshotMetadata) {
_ = json.NewEncoder(os.Stdout).Encode(meta)
}
func exitScreenshotError(context string, err error) {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
os.Exit(1)
}
func formatMime(format screenshot.Format) string {
switch format {
case screenshot.FormatJPEG:
return "image/jpeg"
case screenshot.FormatPPM:
return "image/x-portable-pixmap"
default:
return "image/png"
}
}
func runScreenshot(config screenshot.Config) { func runScreenshot(config screenshot.Config) {
if ssJSON && config.Stdout {
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
os.Exit(1)
}
// Region select needs the keyboard; drop popout grabs for its duration. // Region select needs the keyboard; drop popout grabs for its duration.
result, err := func() (*screenshot.CaptureResult, error) { result, err := func() (*screenshot.CaptureResult, error) {
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion
@@ -215,11 +258,13 @@ func runScreenshot(config screenshot.Config) {
}() }()
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) exitScreenshotError("", err)
os.Exit(1)
} }
if result == nil { if result == nil {
if ssJSON {
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
}
os.Exit(0) os.Exit(0)
} }
@@ -231,8 +276,7 @@ func runScreenshot(config screenshot.Config) {
if config.Stdout { if config.Stdout {
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err) exitScreenshotError(" writing to stdout", err)
os.Exit(1)
} }
return return
} }
@@ -252,22 +296,37 @@ func runScreenshot(config screenshot.Config) {
filePath = filepath.Join(outputDir, filename) filePath = filepath.Join(outputDir, filename)
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil { if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err) exitScreenshotError(" writing file", err)
os.Exit(1)
} }
if !ssJSON {
fmt.Println(filePath) fmt.Println(filePath)
} }
}
if config.Clipboard { if config.Clipboard {
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil { if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err) exitScreenshotError(" copying to clipboard", err)
os.Exit(1)
} }
if !config.SaveFile { if !ssJSON && !config.SaveFile {
fmt.Println("Copied to clipboard") fmt.Println("Copied to clipboard")
} }
} }
if ssJSON {
scale := result.Scale
if scale <= 0 {
scale = 1.0
}
writeScreenshotJSON(screenshotMetadata{
Status: "success",
Path: filePath,
Width: result.Buffer.Width,
Height: result.Buffer.Height,
Scale: scale,
Mime: formatMime(config.Format),
})
}
if config.Notify { if config.Notify {
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
screenshot.SendNotification(screenshot.NotifyResult{ screenshot.SendNotification(screenshot.NotifyResult{
+1 -1
View File
@@ -20,7 +20,7 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
+1 -1
View File
@@ -20,7 +20,7 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
authCmd.AddCommand(authSyncCmd, authResolveLockCmd) authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
+263
View File
@@ -74,6 +74,7 @@ type AuthSettings struct {
EnableU2f bool `json:"enableU2f"` EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"` GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"` GreeterEnableU2f bool `json:"greeterEnableU2f"`
GreeterPamExternallyManaged bool `json:"greeterPamExternallyManaged"`
} }
type SyncAuthOptions struct { type SyncAuthOptions struct {
@@ -236,6 +237,14 @@ func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options S
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err) return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
} }
if settings.GreeterPamExternallyManaged {
if err := removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
logFunc(" /etc/pam.d/greetd is externally managed. Skipping DMS greeter PAM sync.")
return nil
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil { if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err return err
} }
@@ -583,6 +592,260 @@ func buildManagedLockscreenPamContent(baseDirs []string, readFile func(string) (
return b.String(), nil return b.String(), nil
} }
var lockscreenPamCandidateServices = []string{
"login",
"system-auth",
"system-login",
"system-local-login",
"common-auth",
"base-auth",
}
type LockscreenPamServiceInfo struct {
Name string `json:"name"`
Dir string `json:"dir"`
Path string `json:"path"`
HasAuth bool `json:"hasAuth"`
InlineFingerprint bool `json:"inlineFingerprint"`
InlineU2f bool `json:"inlineU2f"`
}
type LockscreenPamValidation struct {
Valid bool `json:"valid"`
Path string `json:"path"`
HasAuth bool `json:"hasAuth"`
InlineFingerprint bool `json:"inlineFingerprint"`
InlineU2f bool `json:"inlineU2f"`
MissingModules []string `json:"missingModules"`
Warnings []string `json:"warnings"`
Errors []string `json:"errors"`
}
type lockscreenPamValidateDeps struct {
baseDirs []string
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
pamModuleExists func(string) bool
}
func defaultValidateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: lockscreenPamBaseDirs,
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: pamModuleExists,
}
}
// lockscreenPamAnalysis is a non-destructive walk of a PAM service. Unlike
// resolveService it detects (rather than strips) pam_fprintd/pam_u2f and
// records unknown directives instead of hard-failing on them.
type lockscreenPamAnalysis struct {
lines []string
hasAuth bool
inlineFingerprint bool
inlineU2f bool
modules []string
unknownDirectives []string
err error
}
func (r lockscreenPamResolver) analyzePath(path string) lockscreenPamAnalysis {
var acc lockscreenPamAnalysis
if err := r.analyzeInto(filepath.Clean(path), "", nil, &acc); err != nil {
acc.err = err
}
return acc
}
func (r lockscreenPamResolver) analyzeInto(path string, filterType string, stack []string, acc *lockscreenPamAnalysis) error {
for _, seen := range stack {
if seen == path {
chain := append(append([]string{}, stack...), path)
display := make([]string, 0, len(chain))
for _, item := range chain {
display = append(display, filepath.Base(item))
}
return fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
}
}
data, err := r.readFile(path)
if err != nil {
return fmt.Errorf("failed to read PAM file %s: %w", path, err)
}
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
rawLine = strings.TrimRight(rawLine, "\r")
trimmed := strings.TrimSpace(rawLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
lineType := pamDirectiveType(trimmed)
if filterType != "" && lineType != "" && lineType != filterType {
continue
}
nestedPath := include.target
if filepath.IsAbs(nestedPath) {
nestedPath = filepath.Clean(nestedPath)
} else {
located, err := r.locate(include.target)
if err != nil {
return fmt.Errorf("failed to read PAM file %s: %w", include.target, err)
}
nestedPath = located
}
if err := r.analyzeInto(nestedPath, include.filterType, append(stack, path), acc); err != nil {
return err
}
continue
}
lineType := pamDirectiveType(trimmed)
if lineType == "" {
acc.unknownDirectives = append(acc.unknownDirectives, trimmed)
continue
}
if filterType != "" && lineType != filterType {
continue
}
acc.lines = append(acc.lines, rawLine)
if lineType == "auth" {
acc.hasAuth = true
}
foundModule := false
for _, field := range strings.Fields(trimmed) {
if strings.HasPrefix(field, "#") {
break
}
if strings.Contains(field, "pam_fprintd") {
acc.inlineFingerprint = true
}
if strings.Contains(field, "pam_u2f") {
acc.inlineU2f = true
}
if !foundModule && strings.HasSuffix(field, ".so") {
acc.modules = append(acc.modules, field)
foundModule = true
}
}
}
return nil
}
// Earlier base dir wins per name (libpam precedence).
func ListLockscreenPamServices() []LockscreenPamServiceInfo {
return listLockscreenPamServices(lockscreenPamBaseDirs, os.ReadFile)
}
func listLockscreenPamServices(baseDirs []string, readFile func(string) ([]byte, error)) []LockscreenPamServiceInfo {
resolver := lockscreenPamResolver{baseDirs: baseDirs, readFile: readFile}
out := make([]LockscreenPamServiceInfo, 0, len(lockscreenPamCandidateServices))
for _, name := range lockscreenPamCandidateServices {
path, err := resolver.locate(name)
if err != nil {
continue
}
info := LockscreenPamServiceInfo{
Name: name,
Dir: filepath.Dir(path),
Path: path,
}
if analysis := resolver.analyzePath(path); analysis.err == nil {
info.HasAuth = analysis.hasAuth
info.InlineFingerprint = analysis.inlineFingerprint
info.InlineU2f = analysis.inlineU2f
}
out = append(out, info)
}
return out
}
func ValidateLockscreenPamService(name string) LockscreenPamValidation {
return validateLockscreenPam(name, "", defaultValidateDeps())
}
func ValidateLockscreenPamPath(path string) LockscreenPamValidation {
return validateLockscreenPam("", path, defaultValidateDeps())
}
func validateLockscreenPam(serviceName string, path string, deps lockscreenPamValidateDeps) LockscreenPamValidation {
result := LockscreenPamValidation{
MissingModules: []string{},
Warnings: []string{},
Errors: []string{},
}
resolver := lockscreenPamResolver{baseDirs: deps.baseDirs, readFile: deps.readFile}
var analysis lockscreenPamAnalysis
if path != "" {
result.Path = path
analysis = resolver.analyzePath(path)
} else {
located, err := resolver.locate(serviceName)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("PAM service %q not found: %v", serviceName, err))
return result
}
result.Path = located
analysis = resolver.analyzePath(located)
}
if analysis.err != nil {
result.Errors = append(result.Errors, analysis.err.Error())
return result
}
result.HasAuth = analysis.hasAuth
result.InlineFingerprint = analysis.inlineFingerprint
result.InlineU2f = analysis.inlineU2f
if !analysis.hasAuth {
result.Errors = append(result.Errors, "no auth directives found after include resolution")
}
for _, directive := range analysis.unknownDirectives {
result.Warnings = append(result.Warnings, "unsupported PAM directive (libpam may still handle it at runtime): "+directive)
}
seen := map[string]bool{}
for _, ref := range analysis.modules {
name := filepath.Base(ref)
if seen[name] {
continue
}
seen[name] = true
if moduleReferenceExists(ref, deps) {
continue
}
result.MissingModules = append(result.MissingModules, name)
result.Warnings = append(result.Warnings, "referenced PAM module not found: "+name)
}
if analysis.inlineFingerprint {
result.Warnings = append(result.Warnings, "pam_fprintd is present in the resolved stack; may double-prompt with DMS's separate fingerprint context")
}
if analysis.inlineU2f {
result.Warnings = append(result.Warnings, "pam_u2f is present in the resolved stack; may double-prompt with DMS's separate U2F context")
}
result.Valid = len(result.Errors) == 0
return result
}
func moduleReferenceExists(ref string, deps lockscreenPamValidateDeps) bool {
if filepath.IsAbs(ref) {
_, err := deps.stat(ref)
return err == nil
}
return deps.pamModuleExists(ref)
}
const UserLockscreenPamService = "dankshell" const UserLockscreenPamService = "dankshell"
func UserLockscreenPamDir() string { func UserLockscreenPamDir() string {
+250
View File
@@ -786,6 +786,223 @@ func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
} }
} }
func (e *pamTestEnv) validateDeps() lockscreenPamValidateDeps {
return lockscreenPamValidateDeps{
baseDirs: []string{e.pamDir},
readFile: os.ReadFile,
stat: os.Stat,
pamModuleExists: func(module string) bool { return e.availableModules[module] },
}
}
func TestListLockscreenPamServices(t *testing.T) {
t.Parallel()
t.Run("dedupes by name with earlier base dir winning", func(t *testing.T) {
t.Parallel()
etcDir := t.TempDir()
vendorDir := t.TempDir()
// login exists in both dirs; system-auth only in the vendor dir.
writeTestFile(t, filepath.Join(etcDir, "login"), "#%PAM-1.0\nauth required pam_unix.so\naccount required pam_unix.so\n")
writeTestFile(t, filepath.Join(vendorDir, "login"), "#%PAM-1.0\nauth required pam_deny.so\n")
writeTestFile(t, filepath.Join(vendorDir, "system-auth"), "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
services := listLockscreenPamServices([]string{etcDir, vendorDir}, os.ReadFile)
if len(services) != 2 {
t.Fatalf("expected 2 services (login, system-auth), got %d: %+v", len(services), services)
}
byName := map[string]LockscreenPamServiceInfo{}
for _, s := range services {
byName[s.Name] = s
}
login, ok := byName["login"]
if !ok {
t.Fatalf("expected login service, got %+v", services)
}
if login.Dir != etcDir || login.Path != filepath.Join(etcDir, "login") {
t.Fatalf("expected login to resolve in earlier dir %s, got dir=%s path=%s", etcDir, login.Dir, login.Path)
}
if !login.HasAuth {
t.Fatalf("expected login to report hasAuth")
}
if byName["system-auth"].Dir != vendorDir {
t.Fatalf("expected system-auth to resolve in vendor dir, got %s", byName["system-auth"].Dir)
}
})
t.Run("include resolution sets hasAuth and detects inline fprintd/u2f", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
services := listLockscreenPamServices([]string{env.pamDir}, os.ReadFile)
var login LockscreenPamServiceInfo
for _, s := range services {
if s.Name == "login" {
login = s
}
}
if !login.HasAuth {
t.Fatalf("expected hasAuth via resolved include")
}
if !login.InlineFingerprint || !login.InlineU2f {
t.Fatalf("expected inline fingerprint and u2f detection, got %+v", login)
}
})
}
func TestValidateLockscreenPam(t *testing.T) {
t.Parallel()
t.Run("valid service with resolved auth", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("login", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid result, got %+v", result)
}
if !result.HasAuth {
t.Fatalf("expected hasAuth true")
}
if len(result.Errors) != 0 {
t.Fatalf("expected no errors, got %v", result.Errors)
}
})
t.Run("path outside base dirs is read directly", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
outside := filepath.Join(t.TempDir(), "custom-pam")
writeTestFile(t, outside, "#%PAM-1.0\nauth sufficient pam_unix.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("", outside, env.validateDeps())
if !result.Valid || result.Path != outside {
t.Fatalf("expected valid result for outside path, got %+v", result)
}
})
t.Run("missing module produces warning and missingModules but stays valid", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth required pam_absent.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid despite missing module, got %+v", result)
}
if len(result.MissingModules) != 1 || result.MissingModules[0] != "pam_absent.so" {
t.Fatalf("expected missing pam_absent.so, got %v", result.MissingModules)
}
if !containsSubstr(result.Warnings, "pam_absent.so") {
t.Fatalf("expected warning about missing module, got %v", result.Warnings)
}
})
t.Run("unknown directive is a warning not an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.availableModules["pam_foo.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nbadtype required pam_foo.so\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid with unknown directive, got %+v", result)
}
if len(result.Errors) != 0 {
t.Fatalf("expected no errors, got %v", result.Errors)
}
if !containsSubstr(result.Warnings, "unsupported PAM directive") {
t.Fatalf("expected unsupported directive warning, got %v", result.Warnings)
}
})
t.Run("cyclic include is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\n")
env.writePamFile(t, "system-auth", "auth include login\n")
result := validateLockscreenPam("login", "", env.validateDeps())
if result.Valid {
t.Fatalf("expected invalid on cyclic include, got %+v", result)
}
if !containsSubstr(result.Errors, "cyclic PAM include detected") {
t.Fatalf("expected cyclic include error, got %v", result.Errors)
}
})
t.Run("no auth directives is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if result.Valid {
t.Fatalf("expected invalid when no auth directives, got %+v", result)
}
if !containsSubstr(result.Errors, "no auth directives") {
t.Fatalf("expected no-auth error, got %v", result.Errors)
}
})
t.Run("missing file is an error", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
result := validateLockscreenPam("", filepath.Join(env.pamDir, "does-not-exist"), env.validateDeps())
if result.Valid || len(result.Errors) == 0 {
t.Fatalf("expected invalid for missing file, got %+v", result)
}
})
t.Run("inline fingerprint and u2f produce warnings", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "system-auth", "#%PAM-1.0\nauth sufficient pam_unix.so\nauth sufficient pam_fprintd.so\nauth sufficient pam_u2f.so cue\naccount required pam_unix.so\n")
result := validateLockscreenPam("system-auth", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid, got %+v", result)
}
if !result.InlineFingerprint || !result.InlineU2f {
t.Fatalf("expected inline flags set, got %+v", result)
}
if !containsSubstr(result.Warnings, "pam_fprintd") || !containsSubstr(result.Warnings, "pam_u2f") {
t.Fatalf("expected double-prompt warnings, got %v", result.Warnings)
}
})
}
func containsSubstr(items []string, substr string) bool {
for _, item := range items {
if strings.Contains(item, substr) {
return true
}
}
return false
}
func TestSyncAuthConfigWithDeps(t *testing.T) { func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel() t.Parallel()
@@ -848,6 +1065,39 @@ func TestSyncAuthConfigWithDeps(t *testing.T) {
} }
}) })
t.Run("externally managed greetd is stripped and greeter sync skipped", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"greeterPamExternallyManaged":true,"greeterEnableFprint":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_fprintd.so max-tries=2 timeout=10\n"+
GreeterPamManagedBlockEnd+"\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
greetd := readFileString(t, env.greetdPath)
if strings.Contains(greetd, GreeterPamManagedBlockStart) || strings.Contains(greetd, "pam_fprintd") {
t.Fatalf("expected DMS-managed block stripped from externally managed greetd:\n%s", greetd)
}
if !strings.Contains(greetd, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd lines to remain:\n%s", greetd)
}
if !containsSubstr(logs, "externally managed") {
t.Fatalf("expected externally-managed skip log, got %v", logs)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) { t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel() t.Parallel()
+5
View File
@@ -178,9 +178,13 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
yInverted := false yInverted := false
var format uint32 var format uint32
scale := 1.0
if r.selection.surface != nil { if r.selection.surface != nil {
yInverted = r.selection.surface.yInverted yInverted = r.selection.surface.yInverted
format = r.selection.surface.screenFormat format = r.selection.surface.screenFormat
if s := r.selection.surface.output.fractionalScale; s > 0 {
scale = s
}
} }
return &CaptureResult{ return &CaptureResult{
@@ -188,6 +192,7 @@ func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
Region: r.result, Region: r.result,
YInverted: yInverted, YInverted: yInverted,
Format: format, Format: format,
Scale: scale,
}, false, nil }, false, nil
} }
+28 -21
View File
@@ -28,6 +28,21 @@ type CaptureResult struct {
Region Region Region Region
YInverted bool YInverted bool
Format uint32 Format uint32
Scale float64
}
func (o *WaylandOutput) effectiveScale() float64 {
scale := o.fractionalScale
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(o.name)
}
if scale <= 0 {
scale = float64(o.scale)
}
if scale <= 0 {
return 1.0
}
return scale
} }
type Screenshoter struct { type Screenshoter struct {
@@ -255,6 +270,7 @@ func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region,
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
@@ -430,6 +446,7 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
Buffer: composite, Buffer: composite,
Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)}, Region: Region{X: int32(minX), Y: int32(minY), Width: int32(totalW), Height: int32(totalH)},
Format: format, Format: format,
Scale: maxScale,
}, nil }, nil
} }
@@ -502,6 +519,7 @@ func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult
if err != nil { if err != nil {
return nil, err return nil, err
} }
result.Scale = output.effectiveScale()
if result.YInverted { if result.YInverted {
result.Buffer.FlipVertical() result.Buffer.FlipVertical()
@@ -604,6 +622,7 @@ func (s *Screenshoter) captureAndCrop(output *WaylandOutput, region Region) (*Ca
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
@@ -612,16 +631,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return s.captureRegionOnTransformedOutput(output, region) return s.captureRegionOnTransformedOutput(output, region)
} }
scale := output.fractionalScale scale := output.effectiveScale()
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int32(float64(region.X-output.x) * scale) localX := int32(float64(region.X-output.x) * scale)
localY := int32(float64(region.Y-output.y) * scale) localY := int32(float64(region.Y-output.y) * scale)
@@ -660,7 +670,12 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
return nil, fmt.Errorf("capture region: %w", err) return nil, fmt.Errorf("capture region: %w", err)
} }
return s.processFrame(frame, region) result, err := s.processFrame(frame, region)
if err != nil {
return nil, err
}
result.Scale = scale
return result, nil
} }
func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) { func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, region Region) (*CaptureResult, error) {
@@ -669,16 +684,7 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r
return nil, err return nil, err
} }
scale := output.fractionalScale scale := output.effectiveScale()
if scale <= 0 && DetectCompositor() == CompositorHyprland {
scale = GetHyprlandMonitorScale(output.name)
}
if scale <= 0 {
scale = float64(output.scale)
}
if scale <= 0 {
scale = 1.0
}
localX := int(float64(region.X-output.x) * scale) localX := int(float64(region.X-output.x) * scale)
localY := int(float64(region.Y-output.y) * scale) localY := int(float64(region.Y-output.y) * scale)
@@ -730,6 +736,7 @@ func (s *Screenshoter) captureRegionOnTransformedOutput(output *WaylandOutput, r
Region: region, Region: region,
YInverted: false, YInverted: false,
Format: result.Format, Format: result.Format,
Scale: scale,
}, nil }, nil
} }
+4
View File
@@ -895,6 +895,10 @@ Singleton {
readonly property bool greeterU2fReady: Processes.greeterU2fReady readonly property bool greeterU2fReady: Processes.greeterU2fReady
readonly property string greeterU2fReason: Processes.greeterU2fReason readonly property string greeterU2fReason: Processes.greeterU2fReason
readonly property string greeterU2fSource: Processes.greeterU2fSource readonly property string greeterU2fSource: Processes.greeterU2fSource
property string lockPamPath: ""
property bool lockPamInlineFprint: false
property bool lockPamInlineU2f: false
property bool greeterPamExternallyManaged: false
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
property bool lockScreenVideoEnabled: false property bool lockScreenVideoEnabled: false
@@ -233,6 +233,7 @@ var SPEC = {
greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" }, greeterLockDateFormat: { def: "", onChange: "markGreeterSyncPending" },
greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" }, greeterFontFamily: { def: "", onChange: "markGreeterSyncPending" },
greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" }, greeterWallpaperFillMode: { def: "", onChange: "markGreeterSyncPending" },
greeterPamExternallyManaged: { def: false, onChange: "markGreeterSyncPending" },
greeterSyncPending: { def: false }, greeterSyncPending: { def: false },
greeterSyncBaseline: { def: {} }, greeterSyncBaseline: { def: {} },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
@@ -455,6 +456,9 @@ var SPEC = {
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
enableU2f: { def: false, onChange: "scheduleAuthApply" }, enableU2f: { def: false, onChange: "scheduleAuthApply" },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
lockPamPath: { def: "" },
lockPamInlineFprint: { def: false },
lockPamInlineU2f: { def: false },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
lockScreenVideoEnabled: { def: false }, lockScreenVideoEnabled: { def: false },
+1 -1
View File
@@ -213,7 +213,7 @@ Item {
} }
Component.onCompleted: { Component.onCompleted: {
dockRecreateDebounce.start(); dockEnabled = true;
loginSoundTimer.start(); loginSoundTimer.start();
osdStartupTimer.start(); osdStartupTimer.start();
+1 -3
View File
@@ -531,9 +531,7 @@ Item {
} }
function next(): void { function next(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) { MprisController.next();
MprisController.activePlayer.next();
}
} }
function stop(): void { function stop(): void {
@@ -33,6 +33,8 @@ Variants {
color: "transparent" color: "transparent"
visible: root.contentReady && !root.surfaceBounce
updatesEnabled: root.renderActive || root._settleFrames > 0 updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
@@ -56,6 +58,8 @@ Variants {
property string source: SessionData.getMonitorWallpaper(modelData.name) || "" property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#") property bool isColorSource: source.startsWith("#")
property bool contentReady: false
property bool surfaceBounce: false
Connections { Connections {
target: SessionData target: SessionData
@@ -94,6 +98,9 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
isInitialized = true; isInitialized = true;
if (!source || isColorSource) {
contentReady = true;
}
} }
property bool isInitialized: false property bool isInitialized: false
@@ -136,9 +143,9 @@ Variants {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
blurWallpaperWindow.visible = false; root.surfaceBounce = true;
Qt.callLater(() => { Qt.callLater(() => {
blurWallpaperWindow.visible = true; root.surfaceBounce = false;
}); });
} }
} }
@@ -246,6 +253,8 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
if (!newSource)
root.contentReady = true;
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -318,6 +327,9 @@ Variants {
if (status === Image.Error) { if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source); log.warn("failed to load active wallpaper for", modelData.name + ":", source);
} }
if (status === Image.Ready || status === Image.Error) {
root.contentReady = true;
}
} }
} }
@@ -44,7 +44,9 @@ PluginComponent {
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Column { Column {
id: detailColumn id: detailColumn
@@ -132,7 +134,11 @@ PluginComponent {
width: connButtonRow.implicitWidth + Theme.spacingM * 2 width: connButtonRow.implicitWidth + Theme.spacingM * 2
readonly property bool isConnected: TailscaleService.connected readonly property bool isConnected: TailscaleService.connected
color: isConnected ? (connButtonArea.containsMouse ? Theme.errorHover : Theme.surfaceLight) : (connButtonArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight) color: {
if (!connButtonArea.containsMouse)
return Theme.surfaceLight;
return isConnected ? Theme.errorHover : Theme.primaryHoverLight;
}
Row { Row {
id: connButtonRow id: connButtonRow
@@ -330,13 +336,15 @@ PluginComponent {
required property var modelData required property var modelData
required property int index required property int index
readonly property bool isSelf: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "")
readonly property bool isExpanded: detailRoot.expandedHostname === modelData.hostname
width: peerListColumn.width width: peerListColumn.width
height: peerCardColumn.implicitHeight + Theme.spacingS * 2 height: peerCardColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "") ? Theme.primaryHoverLight : Theme.surfaceContainerHighest color: peerMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: isSelf ? Theme.primary : Theme.outlineLight
property bool isSelf: modelData.hostname === (TailscaleService.selfNode ? TailscaleService.selfNode.hostname : "") border.width: isSelf ? 2 : 1
property bool isExpanded: detailRoot.expandedHostname === modelData.hostname
Column { Column {
id: peerCardColumn id: peerCardColumn
@@ -354,14 +362,14 @@ PluginComponent {
width: 8 width: 8
height: 8 height: 8
radius: 4 radius: 4
color: modelData.online ? "#4caf50" : Theme.surfaceVariantText color: modelData.online ? Theme.success : Theme.surfaceVariantText
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
StyledText { StyledText {
text: modelData.hostname || "" text: modelData.hostname || ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold font.weight: isSelf ? Font.Medium : Font.Normal
color: Theme.surfaceText color: Theme.surfaceText
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight elide: Text.ElideRight
@@ -370,7 +378,7 @@ PluginComponent {
StyledText { StyledText {
visible: isSelf visible: isSelf
text: I18n.tr("This device", "Label for the user's own device in Tailscale") text: I18n.tr("This device", "Label for the user's own device in Tailscale")
font.pixelSize: 10 font.pixelSize: Theme.fontSizeSmall
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
} }
@@ -409,7 +417,7 @@ PluginComponent {
} }
return parts.join(" \u2022 "); return parts.join(" \u2022 ");
} }
font.pixelSize: 10 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
@@ -429,7 +437,7 @@ PluginComponent {
StyledText { StyledText {
text: modelData.dnsName || "" text: modelData.dnsName || ""
font.pixelSize: 10 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
Layout.fillWidth: true Layout.fillWidth: true
elide: Text.ElideRight elide: Text.ElideRight
@@ -447,14 +455,14 @@ PluginComponent {
StyledText { StyledText {
visible: (modelData.tags || []).length > 0 visible: (modelData.tags || []).length > 0
text: I18n.tr("Tags: %1", "Tailscale device tags").arg((modelData.tags || []).join(", ")) text: I18n.tr("Tags: %1", "Tailscale device tags").arg((modelData.tags || []).join(", "))
font.pixelSize: 10 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
StyledText { StyledText {
visible: (modelData.owner || "").length > 0 visible: (modelData.owner || "").length > 0
text: I18n.tr("Owner: %1", "Tailscale device owner").arg(modelData.owner || "") text: I18n.tr("Owner: %1", "Tailscale device owner").arg(modelData.owner || "")
font.pixelSize: 10 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
+39 -27
View File
@@ -129,7 +129,7 @@ BasePill {
if (deltaY > 0) { if (deltaY > 0) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else { } else {
activePlayer.next(); MprisController.next();
} }
} else { } else {
scrollAccumulatorY += deltaY; scrollAccumulatorY += deltaY;
@@ -137,7 +137,7 @@ BasePill {
if (scrollAccumulatorY > 0) { if (scrollAccumulatorY > 0) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else { } else {
activePlayer.next(); MprisController.next();
} }
scrollAccumulatorY = 0; scrollAccumulatorY = 0;
} }
@@ -266,7 +266,7 @@ BasePill {
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
MprisController.previousOrRewind(); MprisController.previousOrRewind();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
activePlayer.next(); MprisController.next();
} }
} }
} }
@@ -358,38 +358,50 @@ BasePill {
onTextChanged: { onTextChanged: {
scrollOffset = 0; scrollOffset = 0;
textShift = 0; textShift = 0;
scrollTimer.reset();
textChangeAnimation.restart(); textChangeAnimation.restart();
} }
SequentialAnimation { // Timer stepping, not NumberAnimation a running animation commits frames every vsync (#2863)
id: scrollAnimation Timer {
id: scrollTimer
readonly property real maxOffset: Math.max(0, mediaText.implicitWidth - textContainer.width + 5)
property int direction: 1
property int holdTicks: 33
interval: 60
repeat: true
running: mediaText.needsScrolling && textContainer.visible && mediaText.onScreen && root._isPlaying running: mediaText.needsScrolling && textContainer.visible && mediaText.onScreen && root._isPlaying
loops: Animation.Infinite
onStopped: mediaText.scrollOffset = 0
PauseAnimation { function reset() {
duration: 2000 mediaText.scrollOffset = 0;
direction = 1;
holdTicks = 33;
} }
NumberAnimation { onRunningChanged: {
target: mediaText if (!running)
property: "scrollOffset" reset();
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
} }
onTriggered: {
PauseAnimation { if (holdTicks > 0) {
duration: 2000 holdTicks--;
return;
} }
const next = mediaText.scrollOffset + direction;
NumberAnimation { if (next >= maxOffset) {
target: mediaText mediaText.scrollOffset = maxOffset;
property: "scrollOffset" direction = -1;
to: 0 holdTicks = 33;
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60) return;
easing.type: Easing.Linear }
if (next <= 0) {
mediaText.scrollOffset = 0;
direction = 1;
holdTicks = 33;
return;
}
mediaText.scrollOffset = next;
} }
} }
@@ -520,7 +532,7 @@ BasePill {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (activePlayer) { if (activePlayer) {
activePlayer.next(); MprisController.next();
} }
} }
} }
@@ -662,11 +662,12 @@ Item {
weight: 500 weight: 500
} }
MouseArea { StateLayer {
anchors.fill: parent id: playPauseArea
hoverEnabled: true disabled: !root.activePlayer || !root.activePlayer.canTogglePlaying
cursorShape: Qt.PointingHandCursor stateColor: root.onAccent
onClicked: activePlayer && activePlayer.togglePlaying() cornerRadius: parent.radius
onClicked: root.activePlayer.togglePlaying()
} }
ElevationShadow { ElevationShadow {
@@ -706,7 +707,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next() onClicked: MprisController.next()
} }
} }
} }
@@ -190,7 +190,7 @@ Card {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next() onClicked: MprisController.next()
} }
} }
} }
+13
View File
@@ -340,7 +340,19 @@ Variants {
return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide); return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide);
} }
property bool startupRevealDone: false
Timer {
id: startupRevealTimer
interval: 200
running: true
onTriggered: dock.startupRevealDone = true
}
property bool reveal: { property bool reveal: {
if (!startupRevealDone)
return false;
if (_modalRetractActive) if (_modalRetractActive)
return false; return false;
@@ -628,6 +640,7 @@ Variants {
id: dockContainer id: dockContainer
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: dock.startupRevealDone ? 1 : 0
transform: Translate { transform: Translate {
id: dockSlide id: dockSlide
@@ -1635,7 +1635,7 @@ Item {
enabled: MprisController.activePlayer?.canGoNext ?? false enabled: MprisController.activePlayer?.canGoNext ?? false
hoverEnabled: enabled hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.activePlayer?.next() onClicked: MprisController.next()
} }
} }
} }
+34 -4
View File
@@ -90,6 +90,17 @@ Scope {
fprint.checkAvail(); fprint.checkAvail();
} }
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
readonly property bool fprintSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineFprint
readonly property bool u2fSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineU2f
FileView {
id: customPamWatcher
path: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : ""
printErrors: false
}
FileView { FileView {
id: dankshellConfigWatcher id: dankshellConfigWatcher
@@ -148,6 +159,8 @@ Scope {
id: passwd id: passwd
config: { config: {
if (root.customPamActive)
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
if (dankshellConfigWatcher.loaded) if (dankshellConfigWatcher.loaded)
return "dankshell"; return "dankshell";
if (nixosMarker.loaded || root.runningFromNixStore) if (nixosMarker.loaded || root.runningFromNixStore)
@@ -157,6 +170,10 @@ Scope {
return "login"; return "login";
} }
configDirectory: { configDirectory: {
if (root.customPamActive) {
const idx = SettingsData.lockPamPath.lastIndexOf("/");
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
}
if (dankshellConfigWatcher.loaded) if (dankshellConfigWatcher.loaded)
return "/etc/pam.d"; return "/etc/pam.d";
if (nixosMarker.loaded || root.runningFromNixStore) if (nixosMarker.loaded || root.runningFromNixStore)
@@ -248,7 +265,7 @@ Scope {
property int errorTries property int errorTries
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured) { if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByCustomPam) {
abort(); abort();
return; return;
} }
@@ -308,7 +325,7 @@ Scope {
property bool available: SettingsData.lockU2fReady property bool available: SettingsData.lockU2fReady
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) { if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByCustomPam) {
abort(); abort();
return; return;
} }
@@ -318,7 +335,7 @@ Scope {
} }
function startForSecondFactor(): void { function startForSecondFactor(): void {
if (!available || !SettingsData.enableU2f) { if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam) {
root.completeUnlock(); root.completeUnlock();
return; return;
} }
@@ -331,7 +348,7 @@ Scope {
} }
function startForAlternativeAuth(): void { function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active) if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
return; return;
abort(); abort();
root.u2fPending = true; root.u2fPending = true;
@@ -486,6 +503,19 @@ Scope {
u2f.checkAvail(); u2f.checkAvail();
} }
function onLockPamPathChanged(): void {
fprint.checkAvail();
u2f.checkAvail();
}
function onLockPamInlineFprintChanged(): void {
fprint.checkAvail();
}
function onLockPamInlineU2fChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void { function onU2fModeChanged(): void {
if (root.lockSecured) { if (root.lockSecured) {
u2f.abort(); u2f.abort();
@@ -359,6 +359,17 @@ Item {
anchors.fill: parent anchors.fill: parent
active: root.widgetEnabled && root.activeComponent !== null active: root.widgetEnabled && root.activeComponent !== null
sourceComponent: root.activeComponent sourceComponent: root.activeComponent
opacity: 0
NumberAnimation {
id: revealFade
target: contentLoader
property: "opacity"
from: 0
to: 1
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
function reloadComponent() { function reloadComponent() {
active = false; active = false;
@@ -384,6 +395,8 @@ Item {
if (!item) if (!item)
return; return;
revealFade.restart();
if (item.pluginService !== undefined) { if (item.pluginService !== undefined) {
item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService; item.pluginService = root.isInstance ? instanceScopedPluginService : root.pluginService;
} }
+37 -41
View File
@@ -18,57 +18,40 @@ Item {
readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f
function greeterFingerprintDescription() { function greeterFingerprintDescription() {
const source = SettingsData.greeterFingerprintSource; if (SettingsData.greeterPamExternallyManaged)
const reason = SettingsData.greeterFingerprintReason; return "greetd PAM is externally managed";
if (SettingsData.greeterFingerprintSource === "pam")
return I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.", "greeter fingerprint login setting");
if (source === "pam") { switch (SettingsData.greeterFingerprintReason) {
switch (reason) {
case "configured_externally":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login.");
case "missing_enrollment":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet.");
case "missing_reader":
return I18n.tr("PAM provides fingerprint auth, but no reader was detected.");
default:
return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed.");
}
}
switch (reason) {
case "ready": case "ready":
return SettingsData.greeterEnableFprint ? I18n.tr("Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled."); return I18n.tr("Authentication changes apply automatically.", "greeter auth setting description");
case "missing_enrollment": case "missing_enrollment":
if (SettingsData.greeterEnableFprint) return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "greeter fingerprint login setting");
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.");
case "missing_reader": case "missing_reader":
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); return I18n.tr("No fingerprint reader detected.", "fingerprint setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM."); return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.", "greeter fingerprint login setting");
default: default:
return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); return I18n.tr("Fingerprint availability could not be confirmed.", "fingerprint setting status");
} }
} }
function greeterU2fDescription() { function greeterU2fDescription() {
const source = SettingsData.greeterU2fSource; if (SettingsData.greeterPamExternallyManaged)
const reason = SettingsData.greeterU2fReason; return "greetd PAM is externally managed";
if (SettingsData.greeterU2fSource === "pam")
return I18n.tr("PAM already provides security-key auth. Enable this to show it at login.", "greeter security key login setting");
if (source === "pam") { switch (SettingsData.greeterU2fReason) {
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login.");
}
switch (reason) {
case "ready": case "ready":
return SettingsData.greeterEnableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Available."); return I18n.tr("Authentication changes apply automatically.", "greeter auth setting description");
case "missing_key_registration": case "missing_key_registration":
if (SettingsData.greeterEnableU2f) return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM."); return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.", "greeter security key login setting");
default: default:
return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); return I18n.tr("Security-key availability could not be confirmed.", "security key setting status");
} }
} }
@@ -499,6 +482,15 @@ Item {
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
} }
SettingsToggleRow {
settingKey: "greeterPamExternallyManaged"
tags: ["greeter", "pam", "managed", "external", "greetd", "auth"]
text: "greetd PAM is externally managed"
description: "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
checked: SettingsData.greeterPamExternallyManaged
onToggled: checked => SettingsData.set("greeterPamExternallyManaged", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "greeterEnableFprint" settingKey: "greeterEnableFprint"
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"] tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
@@ -506,7 +498,7 @@ Item {
description: root.greeterFingerprintDescription() description: root.greeterFingerprintDescription()
descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableFprint checked: SettingsData.greeterEnableFprint
enabled: root.greeterFprintToggleAvailable enabled: root.greeterFprintToggleAvailable && !SettingsData.greeterPamExternallyManaged
onToggled: checked => SettingsData.set("greeterEnableFprint", checked) onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
} }
@@ -517,7 +509,7 @@ Item {
description: root.greeterU2fDescription() description: root.greeterU2fDescription()
descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.greeterEnableU2f checked: SettingsData.greeterEnableU2f
enabled: root.greeterU2fToggleAvailable enabled: root.greeterU2fToggleAvailable && !SettingsData.greeterPamExternallyManaged
onToggled: checked => SettingsData.set("greeterEnableU2f", checked) onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
} }
} }
@@ -718,9 +710,13 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor hoverEnabled: true
enabled: !root.greeterSyncRunning && !root.greeterInstallActionRunning acceptedButtons: Qt.AllButtons
onClicked: root.runGreeterSync() cursorShape: !root.greeterSyncRunning && !root.greeterInstallActionRunning ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: mouse => {
if (mouse.button === Qt.LeftButton && !root.greeterSyncRunning && !root.greeterInstallActionRunning)
root.runGreeterSync();
}
} }
Row { Row {
+215 -17
View File
@@ -1,5 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
@@ -12,35 +12,81 @@ Item {
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
property var authServices: []
property bool authValidateRunning: false
property bool authValidateOk: false
property bool authValidateWarn: false
property string authValidateMessage: ""
property string authPendingApplyPath: ""
property bool authShowCustom: false
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
readonly property string authCustomLabel: I18n.tr("Custom...", "custom PAM authentication source option")
function authServiceLabel(service) {
const label = service.name.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("-");
return service.dir === "/etc/pam.d" ? label : label + " (" + service.dir + ")";
}
readonly property var authOptions: [authAutoLabel, ...authServices.map(s => authServiceLabel(s)), authCustomLabel]
readonly property string authCurrentValue: {
if (SettingsData.lockPamPath === "")
return authAutoLabel;
const svc = authServices.find(s => s.path === SettingsData.lockPamPath);
return svc ? authServiceLabel(svc) : authCustomLabel;
}
function refreshAuthServices() {
authListServicesProcess.running = true;
}
function applyAutoAuthSource() {
SettingsData.set("lockPamPath", "");
SettingsData.set("lockPamInlineFprint", false);
SettingsData.set("lockPamInlineU2f", false);
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateMessage = "";
}
function validateAndApplyAuthSource(path) {
if (!path)
return;
root.authPendingApplyPath = path;
root.authValidateMessage = "";
root.authValidateOk = false;
root.authValidateWarn = false;
root.authValidateRunning = true;
authValidateProcess.command = ["dms", "auth", "validate", "--path", path, "--json"];
authValidateProcess.running = true;
}
function lockFingerprintDescription() { function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) { switch (SettingsData.lockFingerprintReason) {
case "ready": case "ready":
return SettingsData.enableFprint ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use fingerprint authentication for the lock screen."); return I18n.tr("Use fingerprint authentication for the lock screen.", "lock screen fingerprint setting");
case "missing_enrollment": case "missing_enrollment":
if (SettingsData.enableFprint) return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "lock screen fingerprint setting");
return I18n.tr("Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
case "missing_reader": case "missing_reader":
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); return I18n.tr("No fingerprint reader detected.", "fingerprint setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd."); return I18n.tr("Not available — install fprintd and pam_fprintd.", "lock screen fingerprint setting");
default: default:
return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); return I18n.tr("Fingerprint availability could not be confirmed.", "fingerprint setting status");
} }
} }
function lockU2fDescription() { function lockU2fDescription() {
switch (SettingsData.lockU2fReason) { switch (SettingsData.lockU2fReason) {
case "ready": case "ready":
return SettingsData.enableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting"); return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
case "missing_key_registration": case "missing_key_registration":
if (SettingsData.enableU2f) return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
return I18n.tr("Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f."); return I18n.tr("Not available — install or configure pam_u2f.", "lock screen security key setting");
default: default:
return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); return I18n.tr("Security-key availability could not be confirmed.", "security key setting status");
} }
} }
@@ -48,10 +94,15 @@ Item {
SettingsData.refreshAuthAvailability(); SettingsData.refreshAuthAvailability();
} }
Component.onCompleted: refreshAuthDetection() Component.onCompleted: {
onVisibleChanged: {
if (visible)
refreshAuthDetection(); refreshAuthDetection();
refreshAuthServices();
}
onVisibleChanged: {
if (visible) {
refreshAuthDetection();
refreshAuthServices();
}
} }
FileBrowserModal { FileBrowserModal {
@@ -64,6 +115,71 @@ Item {
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path) onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
} }
Process {
id: authListServicesProcess
command: ["dms", "auth", "list-services", "--json"]
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authListServicesProcess.collected = text || ""
}
onExited: exitCode => {
if (exitCode !== 0) {
root.authServices = [];
return;
}
try {
const data = JSON.parse(authListServicesProcess.collected);
root.authServices = (data && Array.isArray(data.services)) ? data.services.filter(s => s.hasAuth) : [];
} catch (e) {
root.authServices = [];
}
}
}
Process {
id: authValidateProcess
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: authValidateProcess.collected = text || ""
}
onExited: exitCode => {
root.authValidateRunning = false;
root.authValidateOk = false;
root.authValidateWarn = false;
let data = null;
try {
data = JSON.parse(authValidateProcess.collected);
} catch (e) {}
if (!data) {
root.authValidateMessage = "validation failed — is dms in PATH?";
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.authValidateMessage = ["not applied:", ...errs].join("\n");
return;
}
SettingsData.set("lockPamPath", root.authPendingApplyPath);
SettingsData.set("lockPamInlineFprint", data.inlineFingerprint === true);
SettingsData.set("lockPamInlineU2f", data.inlineU2f === true);
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.authValidateOk = true;
root.authValidateWarn = warns.length > 0;
root.authValidateMessage = warns.length > 0 ? ["applied with warnings:", ...warns].join("\n") : "applied";
}
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -207,6 +323,88 @@ Item {
} }
} }
SettingsCard {
width: parent.width
iconName: "key"
title: "Authentication Source"
settingKey: "lockAuthSource"
SettingsDropdownRow {
settingKey: "lockPamPath"
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
text: "Authentication Source"
description: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : "Which PAM service the lock screen uses to authenticate"
options: root.authOptions
currentValue: root.authCurrentValue
onValueChanged: value => {
if (value === root.authAutoLabel) {
root.authShowCustom = false;
root.applyAutoAuthSource();
return;
}
if (value === root.authCustomLabel) {
root.authShowCustom = true;
return;
}
root.authShowCustom = false;
const svc = root.authServices.find(s => root.authServiceLabel(s) === value);
if (svc)
root.validateAndApplyAuthSource(svc.path);
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: root.authShowCustom || root.authCurrentValue === root.authCustomLabel
DankTextField {
id: customPamField
width: parent.width - validatePamButton.width - Theme.spacingS
placeholderText: "/etc/pam.d/my-service"
text: SettingsData.lockPamPath
backgroundColor: Theme.surfaceContainerHighest
}
DankButton {
id: validatePamButton
text: I18n.tr("Apply Changes", "validate and apply custom PAM authentication source")
enabled: !root.authValidateRunning && customPamField.text.trim() !== ""
onClicked: root.validateAndApplyAuthSource(customPamField.text.trim())
}
}
Rectangle {
width: parent.width
height: Math.min(160, authStatusText.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
visible: root.authValidateMessage !== ""
StyledText {
id: authStatusText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.authValidateMessage
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: !root.authValidateOk ? Theme.error : (root.authValidateWarn ? Theme.warning : Theme.surfaceVariantText)
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
StyledText {
visible: (SettingsData.lockPamInlineFprint && SettingsData.enableFprint) || (SettingsData.lockPamInlineU2f && SettingsData.enableU2f)
text: "The pinned PAM stack already prompts for fingerprint or security key, so DMS skips its own prompts"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
}
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "lock" iconName: "lock"
+14 -3
View File
@@ -37,6 +37,8 @@ Variants {
color: "transparent" color: "transparent"
visible: root.contentReady && !root.surfaceBounce
updatesEnabled: root.renderActive || root._settleFrames > 0 updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
@@ -64,6 +66,9 @@ Variants {
property string actualTransitionType: transitionType property string actualTransitionType: transitionType
property bool isInitialized: false property bool isInitialized: false
property bool contentReady: false
property bool surfaceBounce: false
property string scrollMode: SettingsData.wallpaperFillMode property string scrollMode: SettingsData.wallpaperFillMode
property bool scrollingEnabled: scrollMode === "Scrolling" property bool scrollingEnabled: scrollMode === "Scrolling"
property int currentWorkspaceIndex: 0 property int currentWorkspaceIndex: 0
@@ -263,9 +268,9 @@ Variants {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
wallpaperWindow.visible = false; root.surfaceBounce = true;
Qt.callLater(() => { Qt.callLater(() => {
wallpaperWindow.visible = true; root.surfaceBounce = false;
}); });
} }
} }
@@ -506,6 +511,9 @@ Variants {
Component.onCompleted: { Component.onCompleted: {
isInitialized = true; isInitialized = true;
if (!source || isColorSource) {
contentReady = true;
}
if (scrollingEnabled) { if (scrollingEnabled) {
updateWorkspaceData(); updateWorkspaceData();
@@ -574,8 +582,10 @@ Variants {
root.effectActive = false; root.effectActive = false;
root.screenScale = CompositorService.getScreenScale(modelData); root.screenScale = CompositorService.getScreenScale(modelData);
// No status change coming to clear the flag // No status change coming to clear the flag
if (!newSource || currentWallpaper.source.toString() === newSource) if (!newSource || currentWallpaper.source.toString() === newSource) {
root.changePending = false; root.changePending = false;
root.contentReady = true;
}
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
@@ -746,6 +756,7 @@ Variants {
} }
if (status === Image.Ready || status === Image.Error) { if (status === Image.Ready || status === Image.Error) {
root.changePending = false; root.changePending = false;
root.contentReady = true;
} }
} }
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
@@ -80,7 +79,6 @@ Item {
readonly property int displayWorkspaceCount: displayedWorkspaceIds.length readonly property int displayWorkspaceCount: displayedWorkspaceIds.length
readonly property int effectiveColumns: SettingsData.overviewColumns readonly property int effectiveColumns: SettingsData.overviewColumns
readonly property int effectiveRows: Math.max(SettingsData.overviewRows, Math.ceil(displayWorkspaceCount / effectiveColumns))
function getWorkspaceMonitorName(workspaceId) { function getWorkspaceMonitorName(workspaceId) {
if (!allWorkspaces || !workspaceId) if (!allWorkspaces || !workspaceId)
@@ -107,21 +105,53 @@ Item {
} }
} }
function getWorkspaceViewportBounds(workspaceId) { function monitorIpcForWorkspace(workspaceId) {
const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId); const workspace = allWorkspaces?.find(ws => ws?.id === workspaceId);
const mon = workspace?.monitor?.lastIpcObject || monitor?.lastIpcObject || {}; return workspace?.monitor?.lastIpcObject ?? monitor?.lastIpcObject ?? null;
const reserved = mon.reserved || [0, 0, 0, 0]; }
const x = (mon.x ?? 0) + (reserved[0] ?? 0); function monitorLogicalSize(ipc) {
const y = (mon.y ?? 0) + (reserved[1] ?? 0); if (!ipc || !ipc.width || !ipc.height)
const width = Math.max((mon.width ?? monitorPhysicalWidth) - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1); return {
const height = Math.max((mon.height ?? monitorPhysicalHeight) - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1); "width": monitorPhysicalWidth,
const scale = Math.min(root.workspaceImplicitWidth / width, root.workspaceImplicitHeight / height); "height": monitorPhysicalHeight
};
const monScale = ipc.scale > 0 ? ipc.scale : 1;
const rotated = ((ipc.transform ?? 0) % 2) === 1;
return {
"width": (rotated ? ipc.height : ipc.width) / monScale,
"height": (rotated ? ipc.width : ipc.height) / monScale
};
}
function cellForWorkspace(workspaceId) {
const cell = gridLayout.cells.find(c => c.id === workspaceId);
if (cell)
return cell;
return {
"id": workspaceId,
"x": 0,
"y": 0,
"width": workspaceImplicitWidth,
"height": workspaceImplicitHeight
};
}
function getWorkspaceViewportBounds(workspaceId, cellWidth, cellHeight) {
const ipc = monitorIpcForWorkspace(workspaceId) ?? {};
const logical = monitorLogicalSize(ipc);
const reserved = ipc.reserved || [0, 0, 0, 0];
const x = (ipc.x ?? 0) + (reserved[0] ?? 0);
const y = (ipc.y ?? 0) + (reserved[1] ?? 0);
const width = Math.max(logical.width - (reserved[0] ?? 0) - (reserved[2] ?? 0), 1);
const height = Math.max(logical.height - (reserved[1] ?? 0) - (reserved[3] ?? 0), 1);
return { return {
"x": x, "x": x,
"y": y, "y": y,
"scale": scale "scale": Math.min(cellWidth / width, cellHeight / height)
}; };
} }
@@ -143,6 +173,56 @@ Item {
property int draggingFromWorkspace: -1 property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1 property int draggingTargetWorkspace: -1
readonly property var gridLayout: {
const ids = displayedWorkspaceIds;
const columns = effectiveColumns;
if (!ids || ids.length === 0 || columns < 1)
return {
"cells": [],
"width": 0,
"height": 0
};
const rows = [];
for (let i = 0; i < ids.length; i += columns) {
rows.push(ids.slice(i, i + columns).map(id => {
const logical = monitorLogicalSize(monitorIpcForWorkspace(id));
return {
"id": id,
"width": logical.width * scale,
"height": logical.height * scale
};
}));
}
const rowWidth = row => row.reduce((acc, cell) => acc + cell.width, 0) + workspaceSpacing * (row.length - 1);
const totalWidth = rows.reduce((acc, row) => Math.max(acc, rowWidth(row)), 0);
const cells = [];
let cursorY = 0;
for (const row of rows) {
const rowHeight = row.reduce((acc, cell) => Math.max(acc, cell.height), 0);
let cursorX = (totalWidth - rowWidth(row)) / 2;
for (const cell of row) {
cells.push({
"id": cell.id,
"x": cursorX,
"y": cursorY + (rowHeight - cell.height) / 2,
"width": cell.width,
"height": cell.height
});
cursorX += cell.width + workspaceSpacing;
}
cursorY += rowHeight + workspaceSpacing;
}
return {
"cells": cells,
"width": totalWidth,
"height": cursorY - workspaceSpacing
};
}
implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2 implicitWidth: overviewBackground.implicitWidth + Theme.spacingL * 2
implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2 implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2
@@ -166,8 +246,8 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 implicitWidth: workspaceGrid.implicitWidth + padding * 2
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 implicitHeight: workspaceGrid.implicitHeight + padding * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.surfaceContainer
@@ -182,33 +262,27 @@ Item {
shadowEnabled: Theme.elevationEnabled shadowEnabled: Theme.elevationEnabled
} }
ColumnLayout { Item {
id: workspaceColumnLayout id: workspaceGrid
z: root.workspaceZ z: root.workspaceZ
anchors.centerIn: parent anchors.centerIn: parent
spacing: workspaceSpacing implicitWidth: root.gridLayout.width
implicitHeight: root.gridLayout.height
Repeater { Repeater {
model: root.effectiveRows model: root.gridLayout.cells.length
delegate: RowLayout {
id: row
property int rowIndex: index
spacing: workspaceSpacing
Repeater {
model: root.effectiveColumns
Rectangle { Rectangle {
id: workspace id: workspace
property int colIndex: index required property int index
property int workspaceIndex: rowIndex * root.effectiveColumns + colIndex readonly property var cell: root.gridLayout.cells[index] ?? null
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 property int workspaceValue: cell?.id ?? -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null property var workspaceObj: (workspaceExists && Hyprland.workspaces?.values) ? Hyprland.workspaces.values.find(ws => ws?.id === workspaceValue) : null
property bool isActive: workspaceObj?.active ?? false property bool isActive: workspaceObj?.active ?? false
property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true property bool isOnThisMonitor: (workspaceObj && root.monitor) ? (workspaceObj.monitor?.name === root.monitor.name) : true
property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false property bool hasWindows: (workspaceValue > 0) ? root.workspaceHasWindows(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3) property color defaultWorkspaceColor: workspaceExists ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, 0.3)
property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1) property color hoveredWorkspaceColor: Qt.lighter(defaultWorkspaceColor, 1.1)
property color hoveredBorderColor: Theme.surfaceVariant property color hoveredBorderColor: Theme.surfaceVariant
@@ -217,8 +291,10 @@ Item {
visible: workspaceValue !== -1 visible: workspaceValue !== -1
implicitWidth: root.workspaceImplicitWidth x: cell?.x ?? 0
implicitHeight: root.workspaceImplicitHeight y: cell?.y ?? 0
width: cell?.width ?? 0
height: cell?.height ?? 0
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 2 border.width: 2
@@ -226,10 +302,10 @@ Item {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: workspaceValue text: workspace.workspaceValue
font.pixelSize: Theme.fontSizeXLarge * 6 font.pixelSize: Theme.fontSizeXLarge * 6
font.weight: Font.DemiBold font.weight: Font.DemiBold
color: Theme.withAlpha(Theme.surfaceText, workspaceExists ? 0.2 : 0.1) color: Theme.withAlpha(Theme.surfaceText, workspace.workspaceExists ? 0.2 : 0.1)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -241,7 +317,7 @@ Item {
onClicked: { onClicked: {
if (root.draggingTargetWorkspace === -1) { if (root.draggingTargetWorkspace === -1) {
root.overviewOpen = false; root.overviewOpen = false;
HyprlandService.focusWorkspace(workspaceValue); HyprlandService.focusWorkspace(workspace.workspaceValue);
} }
} }
} }
@@ -249,28 +325,26 @@ Item {
DropArea { DropArea {
anchors.fill: parent anchors.fill: parent
onEntered: { onEntered: {
root.draggingTargetWorkspace = workspaceValue; root.draggingTargetWorkspace = workspace.workspaceValue;
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) if (root.draggingFromWorkspace == root.draggingTargetWorkspace)
return; return;
hoveredWhileDragging = true; workspace.hoveredWhileDragging = true;
} }
onExited: { onExited: {
hoveredWhileDragging = false; workspace.hoveredWhileDragging = false;
if (root.draggingTargetWorkspace == workspaceValue) if (root.draggingTargetWorkspace == workspace.workspaceValue)
root.draggingTargetWorkspace = -1; root.draggingTargetWorkspace = -1;
} }
} }
} }
} }
} }
}
}
Item { Item {
id: windowSpace id: windowSpace
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth implicitWidth: workspaceGrid.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight implicitHeight: workspaceGrid.implicitHeight
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -306,41 +380,21 @@ Item {
overviewOpen: root.overviewOpen overviewOpen: root.overviewOpen
readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1 readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1
readonly property var workspaceCell: root.cellForWorkspace(windowWorkspaceId)
function getWorkspaceIndex() { readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId, workspaceCell.width, workspaceCell.height)
if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0)
return 0;
if (!windowWorkspaceId || windowWorkspaceId < 0)
return 0;
try {
for (let i = 0; i < root.displayedWorkspaceIds.length; i++) {
if (root.displayedWorkspaceIds[i] === windowWorkspaceId) {
return i;
}
}
return 0;
} catch (e) {
return 0;
}
}
readonly property int workspaceIndex: getWorkspaceIndex()
readonly property int workspaceColIndex: workspaceIndex % root.effectiveColumns
readonly property int workspaceRowIndex: Math.floor(workspaceIndex / root.effectiveColumns)
readonly property var workspaceBounds: root.getWorkspaceViewportBounds(windowWorkspaceId)
toplevel: modelData toplevel: modelData
scale: root.scale scale: root.scale
monitorDpr: root.dpr monitorDpr: root.dpr
availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceWidth: workspaceCell.width
availableWorkspaceHeight: root.workspaceImplicitHeight availableWorkspaceHeight: workspaceCell.height
contentOriginX: workspaceBounds.x contentOriginX: workspaceBounds.x
contentOriginY: workspaceBounds.y contentOriginY: workspaceBounds.y
contentScale: workspaceBounds.scale contentScale: workspaceBounds.scale
widgetMonitorId: root.monitor.id widgetMonitorId: root.monitor.id
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex xOffset: workspaceCell.x
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex yOffset: workspaceCell.y
z: atInitPosition ? root.windowZ : root.windowDraggingZ z: atInitPosition ? root.windowZ : root.windowDraggingZ
property bool atInitPosition: (initX == x && initY == y) property bool atInitPosition: (initX == x && initY == y)
@@ -409,32 +463,24 @@ Item {
Item { Item {
id: monitorLabelSpace id: monitorLabelSpace
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth implicitWidth: workspaceGrid.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight implicitHeight: workspaceGrid.implicitHeight
z: root.monitorLabelZ z: root.monitorLabelZ
Repeater { Repeater {
model: root.effectiveRows model: root.gridLayout.cells.length
delegate: Item {
id: labelRow
property int rowIndex: index
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
width: parent.width
height: root.workspaceImplicitHeight
Repeater {
model: root.effectiveColumns
delegate: Item { delegate: Item {
id: labelItem id: labelItem
property int colIndex: index required property int index
property int workspaceIndex: labelRow.rowIndex * root.effectiveColumns + colIndex readonly property var cell: root.gridLayout.cells[index] ?? null
property int workspaceValue: (root.displayedWorkspaceIds && workspaceIndex < root.displayedWorkspaceIds.length) ? root.displayedWorkspaceIds[workspaceIndex] : -1 property int workspaceValue: cell?.id ?? -1
property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false property bool workspaceExists: (root.allWorkspaceIds && workspaceValue > 0) ? root.allWorkspaceIds.includes(workspaceValue) : false
property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : "" property string workspaceMonitorName: (workspaceValue > 0) ? root.getWorkspaceMonitorName(workspaceValue) : ""
x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex x: cell?.x ?? 0
width: root.workspaceImplicitWidth y: cell?.y ?? 0
height: root.workspaceImplicitHeight width: cell?.width ?? 0
height: cell?.height ?? 0
Rectangle { Rectangle {
anchors.right: parent.right anchors.right: parent.right
@@ -462,5 +508,3 @@ Item {
} }
} }
} }
}
}
+3 -5
View File
@@ -12,8 +12,6 @@ Singleton {
property bool suppressSound: true property bool suppressSound: true
property bool previousPluggedState: false property bool previousPluggedState: false
readonly property var scale: 100 / SettingsData.batteryChargeLimit
Timer { Timer {
id: startupTimer id: startupTimer
interval: 500 interval: 500
@@ -78,7 +76,7 @@ Singleton {
return 0; return 0;
if (batteryCapacity === 0) { if (batteryCapacity === 0) {
if (usePreferred && preferredDeviceKnown) { if (usePreferred && preferredDeviceKnown) {
const val = Math.min(100, Math.round(preferredDevice.percentage * 100 * scale)); const val = Math.min(100, Math.round(preferredDevice.percentage * 100));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
@@ -88,7 +86,7 @@ Singleton {
if (validBatteries.length === 0) if (validBatteries.length === 0)
return _lastBatteryLevel; return _lastBatteryLevel;
const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length; const avgPercentage = validBatteries.reduce((sum, b) => sum + b.percentage, 0) / validBatteries.length;
const val = Math.min(100, Math.round(avgPercentage * 100 * scale)); const val = Math.min(100, Math.round(avgPercentage * 100));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
@@ -96,7 +94,7 @@ Singleton {
const cap = batteryCapacity; const cap = batteryCapacity;
if (cap === 0) if (cap === 0)
return _lastBatteryLevel; return _lastBatteryLevel;
const val = Math.min(100, Math.round((energy * 100) / cap * scale)); const val = Math.min(100, Math.round((energy * 100) / cap));
_lastBatteryLevel = val; _lastBatteryLevel = val;
return val; return val;
} }
+117 -107
View File
@@ -28,7 +28,6 @@ Singleton {
readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") readonly property string hyprlandSignature: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE")
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
readonly property string swaySocket: Quickshell.env("SWAYSOCK") readonly property string swaySocket: Quickshell.env("SWAYSOCK")
readonly property string scrollSocket: Quickshell.env("SWAYSOCK")
readonly property string miracleSocket: Quickshell.env("MIRACLESOCK") readonly property string miracleSocket: Quickshell.env("MIRACLESOCK")
readonly property string labwcPid: Quickshell.env("LABWC_PID") readonly property string labwcPid: Quickshell.env("LABWC_PID")
readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE") readonly property string mangoSignature: Quickshell.env("MANGO_INSTANCE_SIGNATURE")
@@ -943,122 +942,133 @@ Singleton {
} }
} }
// Primary detection asks the kernel which process owns the $WAYLAND_DISPLAY
// socket the compositor quickshell is actually connected to. Env vars like
// HYPRLAND_INSTANCE_SIGNATURE / MANGO_INSTANCE_SIGNATURE can leak into the
// systemd user environment from previous sessions and lie. Unset
// WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect.
// /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path.
function detectCompositor() { function detectCompositor() {
if (mangoSignature && mangoSignature.length > 0) { const script = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"';
isHyprland = false; Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => {
isNiri = false; const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : "";
isMango = true; const name = _compositorNameFromComm(comm);
isSway = false; if (name) {
isScroll = false; _applyCompositor(name);
isMiracle = false; log.info("Detected", name, "from Wayland socket owner:", comm);
isLabwc = false;
compositor = "mango";
log.info("Detected MangoWM via MANGO_INSTANCE_SIGNATURE");
return; return;
} }
if (comm)
if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) { log.info("Unrecognized Wayland socket owner:", comm, "- falling back to env detection");
isHyprland = true; _detectFromEnv(0);
isNiri = false; }, 0, 3000);
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "hyprland";
log.info("Detected Hyprland");
return;
} }
if (niriSocket && niriSocket.length > 0) { function _compositorNameFromComm(comm) {
Proc.runCommand("niriSocketCheck", ["test", "-S", niriSocket], (output, exitCode) => { switch (comm) {
if (exitCode === 0) { case "niri":
isNiri = true; return "niri";
isHyprland = false; case "hyprland":
isMango = false; return "hyprland";
isSway = false; case "sway":
isScroll = false; return "sway";
isMiracle = false; case "scroll":
isLabwc = false; return "scroll";
compositor = "niri"; case "mango":
log.info("Detected Niri with socket:", niriSocket); return "mango";
case "miracle-wm":
return "miracle";
case "labwc":
return "labwc";
default:
return "";
}
}
function _applyCompositor(name) {
isHyprland = name === "hyprland";
isNiri = name === "niri";
isMango = name === "mango";
isSway = name === "sway";
isScroll = name === "scroll";
isMiracle = name === "miracle";
isLabwc = name === "labwc";
compositor = name;
compositorDetected = true;
if (isNiri)
NiriService.generateNiriBlurrule(); NiriService.generateNiriBlurrule();
} }
// Fallback when the socket owner can't be resolved (no ss, unrecognized
// comm). Same priority order as before, but every candidate must prove
// liveness; a dead socket/PID falls through to the next candidate instead
// of winning on a stale env var.
function _envDetectionCandidates() {
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || "";
return [
{
name: "mango",
present: !!mangoSignature,
test: ["test", "-S", mangoSignature],
detail: "MANGO_INSTANCE_SIGNATURE " + mangoSignature
},
{
name: "niri",
present: !!niriSocket,
test: ["test", "-S", niriSocket],
detail: "NIRI_SOCKET " + niriSocket
},
{
name: "miracle",
present: !!miracleSocket,
test: ["test", "-S", miracleSocket],
detail: "MIRACLESOCK " + miracleSocket
},
{
name: "sway",
present: !!swaySocket,
test: ["test", "-S", swaySocket],
resolve: () => {
const desktop = String(Quickshell.env("XDG_CURRENT_DESKTOP") || "").toLowerCase();
return desktop.includes("sway") ? "sway" : "scroll";
},
detail: "SWAYSOCK " + swaySocket
},
{
name: "labwc",
present: !!labwcPid,
test: ["sh", "-c", "[ \"$(cat /proc/\"$LABWC_PID\"/comm 2>/dev/null)\" = labwc ]"],
detail: "LABWC_PID " + labwcPid
},
{
name: "hyprland",
present: !!hyprlandSignature,
test: ["test", "-S", runtimeDir + "/hypr/" + hyprlandSignature + "/.socket.sock"],
detail: "HYPRLAND_INSTANCE_SIGNATURE " + hyprlandSignature
}
];
}
function _detectFromEnv(index) {
const candidates = _envDetectionCandidates();
for (let i = index; i < candidates.length; i++) {
const c = candidates[i];
if (!c.present)
continue;
const next = i + 1;
Proc.runCommand(c.name + "SocketCheck", c.test, (output, exitCode) => {
if (exitCode !== 0) {
log.warn(c.detail, "is set but not alive, skipping");
_detectFromEnv(next);
return;
}
const name = c.resolve ? c.resolve() : c.name;
_applyCompositor(name);
log.info("Detected", name, "via", c.detail);
}, 0); }, 0);
return; return;
} }
_applyCompositor("unknown");
if (swaySocket && swaySocket.length > 0 && !scrollSocket && scrollSocket.length == 0 && !miracleSocket) {
Proc.runCommand("swaySocketCheck", ["test", "-S", swaySocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isSway = true;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "sway";
log.info("Detected Sway with socket:", swaySocket);
}
}, 0);
return;
}
if (miracleSocket && miracleSocket.length > 0) {
Proc.runCommand("miracleSocketCheck", ["test", "-S", miracleSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = true;
isLabwc = false;
compositor = "miracle";
log.info("Detected Miracle WM with socket:", miracleSocket);
}
}, 0);
return;
}
if (scrollSocket && scrollSocket.length > 0 && !miracleSocket) {
Proc.runCommand("scrollSocketCheck", ["test", "-S", scrollSocket], (output, exitCode) => {
if (exitCode === 0) {
isNiri = false;
isHyprland = false;
isMango = false;
isSway = false;
isScroll = true;
isMiracle = false;
isLabwc = false;
compositor = "scroll";
log.info("Detected Scroll with socket:", scrollSocket);
}
}, 0);
return;
}
if (labwcPid && labwcPid.length > 0) {
isHyprland = false;
isNiri = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = true;
compositor = "labwc";
log.info("Detected LabWC with PID:", labwcPid);
return;
}
isHyprland = false;
isNiri = false;
isMango = false;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "unknown";
log.warn("No compositor detected"); log.warn("No compositor detected");
} }
+9 -24
View File
@@ -48,22 +48,12 @@ Singleton {
target: root.activePlayer target: root.activePlayer
function onTrackTitleChanged() { function onTrackTitleChanged() {
root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0; root.activePlayerStableLength = (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) ? root.activePlayer.length : 0;
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
function onTrackArtistChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
} }
function onLengthChanged() { function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length; root.activePlayerStableLength = root.activePlayer.length;
} }
} }
function onPlaybackStateChanged() {
if (root.isIdle(root.activePlayer))
root._resolveActivePlayer();
}
} }
onActivePlayerChanged: { onActivePlayerChanged: {
@@ -73,18 +63,6 @@ Singleton {
onAvailablePlayersChanged: _resolveActivePlayer() onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer() Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function isIdle(player: MprisPlayer): bool { function isIdle(player: MprisPlayer): bool {
return player return player
&& player.playbackState === MprisPlaybackState.Stopped && player.playbackState === MprisPlaybackState.Stopped
@@ -93,14 +71,15 @@ Singleton {
} }
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
// Keep the selected player stable across transient metadata changes.
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
const playing = availablePlayers.find(p => p.isPlaying); const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
activePlayer = playing; activePlayer = playing;
_persistIdentity(playing.identity); _persistIdentity(playing.identity);
return; return;
} }
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0 && !isIdle(activePlayer))
return;
const savedId = SessionData.lastPlayerIdentity; const savedId = SessionData.lastPlayerIdentity;
if (savedId) { if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId); const match = availablePlayers.find(p => p.identity === savedId);
@@ -150,4 +129,10 @@ Singleton {
else if (activePlayer.canGoPrevious) else if (activePlayer.canGoPrevious)
activePlayer.previous(); activePlayer.previous();
} }
function next(): void {
const player = activePlayer;
if (player?.canGoNext)
player.next();
}
} }
+28 -24
View File
@@ -56,13 +56,13 @@ Singleton {
return ""; return "";
} }
function _commit(u) { function _commit(u, artKey, srcUrl) {
resolvedArtUrl = u; resolvedArtUrl = u;
_committedArtKey = u !== "" ? _pendingArtKey : ""; _committedArtKey = u !== "" ? artKey : "";
_committedSrcUrl = u !== "" ? _lastArtUrl : ""; _committedSrcUrl = u !== "" ? srcUrl : "";
} }
function loadArtwork(url) { function loadArtwork(url, artKey, requestSerial) {
if (!url || url === "") { if (!url || url === "") {
// Keep stale art; only blank once the empty url debounce settles. // Keep stale art; only blank once the empty url debounce settles.
_lastArtUrl = ""; _lastArtUrl = "";
@@ -85,11 +85,11 @@ Singleton {
// 1. First, check if the file already exists locally // 1. First, check if the file already exists locally
Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => { Proc.runCommand(null, ["test", "-f", filePath], (output, exitCode) => {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
return; return;
if (exitCode === 0) { if (exitCode === 0) {
_commit(localFileUrl); _commit(localFileUrl, artKey, targetUrl);
loading = false; loading = false;
} else { } else {
const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }"; const dlCmd = "mkdir -p \"$(dirname \"$1\")\" && curl -f -s -L -o \"$1\" \"$2\" && mv \"$1\" \"$3\" || { rm -f \"$1\"; exit 1; }";
@@ -102,18 +102,18 @@ Singleton {
const tmpPath = filePath + ".tmp"; const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, maxresUrl, filePath], (maxOutput, maxExitCode) => {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
return; return;
if (maxExitCode === 0) { if (maxExitCode === 0) {
_commit(localFileUrl); _commit(localFileUrl, artKey, targetUrl);
loading = false; loading = false;
} else { } else {
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, mqUrl, filePath], (mqOutput, mqExitCode) => {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
return; return;
_commit(mqExitCode === 0 ? localFileUrl : targetUrl); _commit(mqExitCode === 0 ? localFileUrl : targetUrl, artKey, targetUrl);
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -122,10 +122,10 @@ Singleton {
// Standard curl download for other remote URLs (e.g. SoundCloud) // Standard curl download for other remote URLs (e.g. SoundCloud)
const tmpPath = filePath + ".tmp"; const tmpPath = filePath + ".tmp";
Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => { Proc.runCommand(null, ["sh", "-c", dlCmd, "sh", tmpPath, targetUrl, filePath], (dlOutput, dlExitCode) => {
if (_lastArtUrl !== targetUrl) if (_lastArtUrl !== targetUrl || _requestSerial !== requestSerial)
return; return;
_commit(dlExitCode === 0 ? localFileUrl : targetUrl); _commit(dlExitCode === 0 ? localFileUrl : targetUrl, artKey, targetUrl);
loading = false; loading = false;
}, 50, 15000); }, 50, 15000);
} }
@@ -141,7 +141,7 @@ Singleton {
// Cover lands after metadata, so poll; commit a content-addressed copy so identical bytes keep an identical url // Cover lands after metadata, so poll; commit a content-addressed copy so identical bytes keep an identical url
const script = "f=\"$1\"; d=\"$2\"; i=0; while [ ! -f \"$f\" ] && [ \"$i\" -lt 20 ]; do sleep 0.15; i=$((i + 1)); done; [ -f \"$f\" ] || exit 1; s=$(sha1sum \"$f\" | cut -c1-40); if [ ! -f \"$d/art_$s\" ]; then mkdir -p \"$d\" && cp \"$f\" \"$d/art_$s.tmp\" && mv \"$d/art_$s.tmp\" \"$d/art_$s\" || exit 1; fi; echo \"$s\""; const script = "f=\"$1\"; d=\"$2\"; i=0; while [ ! -f \"$f\" ] && [ \"$i\" -lt 20 ]; do sleep 0.15; i=$((i + 1)); done; [ -f \"$f\" ] || exit 1; s=$(sha1sum \"$f\" | cut -c1-40); if [ ! -f \"$d/art_$s\" ]; then mkdir -p \"$d\" && cp \"$f\" \"$d/art_$s.tmp\" && mv \"$d/art_$s.tmp\" \"$d/art_$s\" || exit 1; fi; echo \"$s\"";
Proc.runCommand(null, ["sh", "-c", script, "sh", filePath, cacheDir], (output, exitCode) => { Proc.runCommand(null, ["sh", "-c", script, "sh", filePath, cacheDir], (output, exitCode) => {
if (_lastArtUrl !== localUrl) if (_lastArtUrl !== localUrl || _requestSerial !== requestSerial)
return; return;
loading = false; loading = false;
if (exitCode !== 0) if (exitCode !== 0)
@@ -149,7 +149,7 @@ Singleton {
const sha = (output || "").trim(); const sha = (output || "").trim();
if (_artHashDenylist.indexOf(sha) !== -1) if (_artHashDenylist.indexOf(sha) !== -1)
return; return;
_commit("file://" + cacheDir + "/art_" + sha); _commit("file://" + cacheDir + "/art_" + sha, artKey, localUrl);
}, 50, 5000); }, 50, 5000);
} }
@@ -158,7 +158,7 @@ Singleton {
interval: 800 interval: 800
onTriggered: { onTriggered: {
if (root._lastArtUrl === "") if (root._lastArtUrl === "")
root._commit(""); root._commit("", "", "");
} }
} }
@@ -167,6 +167,7 @@ Singleton {
property string _committedArtKey: "" property string _committedArtKey: ""
property string _pendingArtKey: "" property string _pendingArtKey: ""
property string _committedSrcUrl: "" property string _committedSrcUrl: ""
property int _requestSerial: 0
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
@@ -182,9 +183,10 @@ Singleton {
const p = activePlayer; const p = activePlayer;
if (!p) if (!p)
return ""; return "";
// +title/artist: KDEconnect reuses one trackid forever; album excluded: Chrome delivers it late, orphaning the lock // Scope artwork ownership to the MPRIS object and track.
const playerId = p.uniqueId || p.identity || "";
const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : ""; const tid = p.metadata && p.metadata["mpris:trackid"] ? p.metadata["mpris:trackid"].toString() : "";
return tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || ""); return playerId + " " + tid + " " + (p.trackTitle || "") + " " + (p.trackArtist || "");
} }
function artReadyFor(player) { function artReadyFor(player) {
@@ -194,17 +196,19 @@ Singleton {
function _updateArtUrl() { function _updateArtUrl() {
const key = _trackKey(); const key = _trackKey();
// Skip once real art is committed for this track (dedup Chrome's multi-size if (key !== _pendingArtKey) {
// re-publish). The lock is set in _commit(), never optimistically, so a rejected _requestSerial++;
// placeholder or a short-circuited duplicate url can't wedge the real cover out. loading = false;
if (key !== "" && key === _committedArtKey) }
return;
_pendingArtKey = key; _pendingArtKey = key;
const url = getArtworkUrl(activePlayer); const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates.
if (key !== "" && key === _committedArtKey)
return;
if (key !== "" && url !== "" && url === _committedSrcUrl) { if (key !== "" && url !== "" && url === _committedSrcUrl) {
_committedArtKey = key; // Chrome can publish track metadata before its new artwork URL.
return; return;
} }
loadArtwork(url); loadArtwork(url, key, _requestSerial);
} }
} }
+6 -10
View File
@@ -182,16 +182,12 @@ Item {
} }
} }
FrameAnimation { // Timer, not FrameAnimation a running animation commits frames every vsync (#2863)
running: blobEffect.visible Timer {
property real pending: 0 running: blobEffect.visible && root.onScreen
onTriggered: { interval: 33
pending += frameTime; repeat: true
if (pending < 0.03) onTriggered: root.stepBlob(0.033)
return;
root.stepBlob(Math.min(pending, 0.05));
pending = 0;
}
} }
ShaderEffect { ShaderEffect {
+4
View File
@@ -147,6 +147,10 @@ Item {
trackColor: MediaAccentService.accentTrack trackColor: MediaAccentService.accentTrack
actualProgressColor: MediaAccentService.accentSubtle actualProgressColor: MediaAccentService.accentSubtle
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
onFrameTicked: {
if (!root.isSeeking)
activePlayer.positionChanged();
}
MouseArea { MouseArea {
id: waveMouseArea id: waveMouseArea
+7 -3
View File
@@ -50,11 +50,15 @@ Item {
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb") fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
} }
signal frameTicked
FrameAnimation { FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) running: root.visible && (root.isPlaying || root.currentAmp > 0) && (root.Window.window?.visible ?? false)
onTriggered: { onTriggered: {
if (root.isPlaying) if (!root.isPlaying)
root.phase += 0.03 * frameTime * 60; return;
root.phase = (root.phase + 0.03 * frameTime * 60) % 6.28318530718;
root.frameTicked();
} }
} }
} }
File diff suppressed because it is too large Load Diff
+25 -25
View File
@@ -777,7 +777,7 @@
"Authenticate": "صادِق" "Authenticate": "صادِق"
}, },
"Authenticated!": { "Authenticated!": {
"Authenticated!": "" "Authenticated!": "تم التحقق بنجاح!"
}, },
"Authenticating...": { "Authenticating...": {
"Authenticating...": "جاري المصادقة..." "Authenticating...": "جاري المصادقة..."
@@ -804,13 +804,13 @@
"Authentication error - try again": "خطأ في المصادقة - حاول مرة أخرى" "Authentication error - try again": "خطأ في المصادقة - حاول مرة أخرى"
}, },
"Authentication failed - attempt %1 of %2": { "Authentication failed - attempt %1 of %2": {
"Authentication failed - attempt %1 of %2": "" "Authentication failed - attempt %1 of %2": "فشل التحقق من الهوية - المحاولة %1 من %2"
}, },
"Authentication failed - lockout can occur": { "Authentication failed - lockout can occur": {
"Authentication failed - lockout can occur": "" "Authentication failed - lockout can occur": "فشل التحقق - قد يؤدي ذلك إلى قفل حسابك"
}, },
"Authentication failed - try again": { "Authentication failed - try again": {
"Authentication failed - try again": "" "Authentication failed - try again": "فشل التحقق - يرجى المحاولة مرة أخرى"
}, },
"Authentication failed, please try again": { "Authentication failed, please try again": {
"Authentication failed, please try again": "فشل التحقق، أعد المحاولة" "Authentication failed, please try again": "فشل التحقق، أعد المحاولة"
@@ -987,13 +987,13 @@
"Available.": "متاح." "Available.": "متاح."
}, },
"Awaiting fingerprint authentication": { "Awaiting fingerprint authentication": {
"Awaiting fingerprint authentication": "" "Awaiting fingerprint authentication": "بانتظار التحقق ببصمة الإصبع"
}, },
"Awaiting fingerprint or security key authentication": { "Awaiting fingerprint or security key authentication": {
"Awaiting fingerprint or security key authentication": "" "Awaiting fingerprint or security key authentication": "بانتظار التحقق ببصمة الإصبع أو مفتاح الأمان"
}, },
"Awaiting security key authentication": { "Awaiting security key authentication": {
"Awaiting security key authentication": "" "Awaiting security key authentication": "بانتظار التحقق بمفتاح الأمان"
}, },
"BSSID": { "BSSID": {
"BSSID": "BSSID" "BSSID": "BSSID"
@@ -2175,7 +2175,7 @@
"Custom command and terminal params are split on whitespace; paths with spaces will break.": "يتم فصل الأمر المخصص ومعلمات الجهاز على المسافات البيضاء؛ المسارات التي تحتوي على مسافات ستتسبب في خطأ." "Custom command and terminal params are split on whitespace; paths with spaces will break.": "يتم فصل الأمر المخصص ومعلمات الجهاز على المسافات البيضاء؛ المسارات التي تحتوي على مسافات ستتسبب في خطأ."
}, },
"Custom interval in minutes (minimum 5)": { "Custom interval in minutes (minimum 5)": {
"Custom interval in minutes (minimum 5)": "" "Custom interval in minutes (minimum 5)": "الفترة الزمنية المخصصة بالدقائق (الحد الأدنى 5)"
}, },
"Custom open-trash command": { "Custom open-trash command": {
"Custom open-trash command": "أمر مخصص لفتح المهملات" "Custom open-trash command": "أمر مخصص لفتح المهملات"
@@ -3435,7 +3435,7 @@
"Flags": "الأعلام" "Flags": "الأعلام"
}, },
"Flatpak": { "Flatpak": {
"Flatpak": "" "Flatpak": "Flatpak"
}, },
"Flipped": { "Flipped": {
"Flipped": "معكوس" "Flipped": "معكوس"
@@ -3852,7 +3852,7 @@
"Groups": "المجموعات" "Groups": "المجموعات"
}, },
"H": { "H": {
"H": "" "H": "H"
}, },
"HDR (EDID)": { "HDR (EDID)": {
"HDR (EDID)": "HDR (EDID)" "HDR (EDID)": "HDR (EDID)"
@@ -4149,22 +4149,22 @@
"Ignore Completely": "تجاهل تمامًا" "Ignore Completely": "تجاهل تمامًا"
}, },
"Ignore package": { "Ignore package": {
"Ignore package": "" "Ignore package": "تجاهل البرنامج"
}, },
"Ignore this package": { "Ignore this package": {
"Ignore this package": "" "Ignore this package": "تجاهل هذا التطبيق"
}, },
"Ignored (%1)": { "Ignored (%1)": {
"Ignored (%1)": "" "Ignored (%1)": "تم تجاهله (%1)"
}, },
"Ignored Packages": { "Ignored Packages": {
"Ignored Packages": "" "Ignored Packages": "التطبيقات المتجاهلة"
}, },
"Ignored packages are hidden from the updater and skipped by 'Update All'.": { "Ignored packages are hidden from the updater and skipped by 'Update All'.": {
"Ignored packages are hidden from the updater and skipped by 'Update All'.": "" "Ignored packages are hidden from the updater and skipped by 'Update All'.": "يتم إخفاء التطبيقات المتجاهلة من أداة التحديث ويتخطاها خيار \"تحديث الكل\"."
}, },
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": { "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": {
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "" "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "تنطبق التطبيقات المتجاهلة فقط على أداة التحديث المدمجة. يتحكم أمرك المخصص في الاستثناءات الخاصة به."
}, },
"Image": { "Image": {
"Image": "صورة" "Image": "صورة"
@@ -4347,7 +4347,7 @@
"Invalid configuration": "تكوين غير صالح" "Invalid configuration": "تكوين غير صالح"
}, },
"Invalid package name — letters, digits and @._+:- only.": { "Invalid package name — letters, digits and @._+:- only.": {
"Invalid package name — letters, digits and @._+:- only.": "" "Invalid package name — letters, digits and @._+:- only.": "اسم التطبيق غير صالح — يُسمح فقط بالأحرف، والأرقام، والرموز التالية: @._+:-"
}, },
"Invalid password for %1": { "Invalid password for %1": {
"Invalid password for %1": "كلمة مرور غير صالحة لـ %1" "Invalid password for %1": "كلمة مرور غير صالحة لـ %1"
@@ -5451,7 +5451,7 @@
"No output devices found": "لم يتم العثور على أجهزة إخراج" "No output devices found": "لم يتم العثور على أجهزة إخراج"
}, },
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": { "No packages ignored. Add one here or hover an update in the popout and click the hide button.": {
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": "" "No packages ignored. Add one here or hover an update in the popout and click the hide button.": "لا توجد تطبيقات متجاهلة. أضف تطبيقًا هنا، أو مرر مؤشر الفأرة فوق التحديث في النافذة المنبثقة وانقر على زر الإخفاء."
}, },
"No peers found": { "No peers found": {
"No peers found": "لم يتم العثور على أقران" "No peers found": "لم يتم العثور على أقران"
@@ -5937,7 +5937,7 @@
"PIN": "PIN" "PIN": "PIN"
}, },
"Package name (e.g., docker)": { "Package name (e.g., docker)": {
"Package name (e.g., docker)": "" "Package name (e.g., docker)": "اسم التطبيق (مثال: docker)"
}, },
"Pad": { "Pad": {
"Pad": "وسادة" "Pad": "وسادة"
@@ -7812,7 +7812,7 @@
"Status": "الحالة" "Status": "الحالة"
}, },
"Stop ignoring %1": { "Stop ignoring %1": {
"Stop ignoring %1": "" "Stop ignoring %1": "إلغاء تجاهل %1"
}, },
"Stopped": { "Stopped": {
"Stopped": "متوقف" "Stopped": "متوقف"
@@ -8910,7 +8910,7 @@
"Votes": "الأصوات" "Votes": "الأصوات"
}, },
"W": { "W": {
"W": "" "W": "W"
}, },
"WPA/WPA2": { "WPA/WPA2": {
"WPA/WPA2": "WPA/WPA2" "WPA/WPA2": "WPA/WPA2"
@@ -9213,16 +9213,16 @@
"by %1": "بواسطة %1" "by %1": "بواسطة %1"
}, },
"checked %1d ago": { "checked %1d ago": {
"checked %1d ago": "" "checked %1d ago": "تم الفحص قبل %1 من الأيام"
}, },
"checked %1h ago": { "checked %1h ago": {
"checked %1h ago": "" "checked %1h ago": "تم الفحص قبل %1 من الساعات"
}, },
"checked %1m ago": { "checked %1m ago": {
"checked %1m ago": "" "checked %1m ago": "تم الفحص قبل %1 من الدقائق"
}, },
"checked just now": { "checked just now": {
"checked just now": "" "checked just now": "تم الفحص الآن"
}, },
"days": { "days": {
"days": "أيام" "days": "أيام"
+25 -25
View File
@@ -777,7 +777,7 @@
"Authenticate": "Authentifizieren" "Authenticate": "Authentifizieren"
}, },
"Authenticated!": { "Authenticated!": {
"Authenticated!": "" "Authenticated!": "Authentifiziert!"
}, },
"Authenticating...": { "Authenticating...": {
"Authenticating...": "Authentifizierung..." "Authenticating...": "Authentifizierung..."
@@ -804,13 +804,13 @@
"Authentication error - try again": "Authentifizierungsfehler - erneut versuchen" "Authentication error - try again": "Authentifizierungsfehler - erneut versuchen"
}, },
"Authentication failed - attempt %1 of %2": { "Authentication failed - attempt %1 of %2": {
"Authentication failed - attempt %1 of %2": "" "Authentication failed - attempt %1 of %2": "Authentifizierung fehlgeschlagen - Versuch %1 von %2"
}, },
"Authentication failed - lockout can occur": { "Authentication failed - lockout can occur": {
"Authentication failed - lockout can occur": "" "Authentication failed - lockout can occur": "Authentifizierung fehlgeschlagen - Sperrung kann erfolgen"
}, },
"Authentication failed - try again": { "Authentication failed - try again": {
"Authentication failed - try again": "" "Authentication failed - try again": "Authentifizierung fehlgeschlagen - erneut versuchen"
}, },
"Authentication failed, please try again": { "Authentication failed, please try again": {
"Authentication failed, please try again": "Authentifizierung nicht erfolgreich, bitte nochmals probieren" "Authentication failed, please try again": "Authentifizierung nicht erfolgreich, bitte nochmals probieren"
@@ -987,13 +987,13 @@
"Available.": "Verfügbar." "Available.": "Verfügbar."
}, },
"Awaiting fingerprint authentication": { "Awaiting fingerprint authentication": {
"Awaiting fingerprint authentication": "" "Awaiting fingerprint authentication": "Warten auf Fingerabdruck-Authentifizierung"
}, },
"Awaiting fingerprint or security key authentication": { "Awaiting fingerprint or security key authentication": {
"Awaiting fingerprint or security key authentication": "" "Awaiting fingerprint or security key authentication": "Warten auf Fingerabdruck- oder Sicherheitsschlüssel-Authentifizierung"
}, },
"Awaiting security key authentication": { "Awaiting security key authentication": {
"Awaiting security key authentication": "" "Awaiting security key authentication": "Warten auf Sicherheitsschlüssel-Authentifizierung"
}, },
"BSSID": { "BSSID": {
"BSSID": "BSSID" "BSSID": "BSSID"
@@ -2175,7 +2175,7 @@
"Custom command and terminal params are split on whitespace; paths with spaces will break.": "Benutzerdefinierte Befehle und Terminalparameter werden an Leerzeichen getrennt; Pfade mit Leerzeichen funktionieren nicht." "Custom command and terminal params are split on whitespace; paths with spaces will break.": "Benutzerdefinierte Befehle und Terminalparameter werden an Leerzeichen getrennt; Pfade mit Leerzeichen funktionieren nicht."
}, },
"Custom interval in minutes (minimum 5)": { "Custom interval in minutes (minimum 5)": {
"Custom interval in minutes (minimum 5)": "" "Custom interval in minutes (minimum 5)": "Benutzerdefiniertes Intervall in Minuten (Minimum 5)"
}, },
"Custom open-trash command": { "Custom open-trash command": {
"Custom open-trash command": "Benutzerdefinierter Befehl zum Öffnen des Papierkorbs" "Custom open-trash command": "Benutzerdefinierter Befehl zum Öffnen des Papierkorbs"
@@ -3435,7 +3435,7 @@
"Flags": "Flags" "Flags": "Flags"
}, },
"Flatpak": { "Flatpak": {
"Flatpak": "" "Flatpak": "Flatpak"
}, },
"Flipped": { "Flipped": {
"Flipped": "Gespiegelt" "Flipped": "Gespiegelt"
@@ -3852,7 +3852,7 @@
"Groups": "Gruppen" "Groups": "Gruppen"
}, },
"H": { "H": {
"H": "" "H": "H"
}, },
"HDR (EDID)": { "HDR (EDID)": {
"HDR (EDID)": "HDR (EDID)" "HDR (EDID)": "HDR (EDID)"
@@ -4149,22 +4149,22 @@
"Ignore Completely": "Vollständig ignorieren" "Ignore Completely": "Vollständig ignorieren"
}, },
"Ignore package": { "Ignore package": {
"Ignore package": "" "Ignore package": "Paket ignorieren"
}, },
"Ignore this package": { "Ignore this package": {
"Ignore this package": "" "Ignore this package": "Dieses Paket ignorieren"
}, },
"Ignored (%1)": { "Ignored (%1)": {
"Ignored (%1)": "" "Ignored (%1)": "Ignoriert (%1)"
}, },
"Ignored Packages": { "Ignored Packages": {
"Ignored Packages": "" "Ignored Packages": "Ignorierte Pakete"
}, },
"Ignored packages are hidden from the updater and skipped by 'Update All'.": { "Ignored packages are hidden from the updater and skipped by 'Update All'.": {
"Ignored packages are hidden from the updater and skipped by 'Update All'.": "" "Ignored packages are hidden from the updater and skipped by 'Update All'.": "Ignorierte Pakete werden im Updater ausgeblendet und bei \"Alle aktualisieren\" übersprungen."
}, },
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": { "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": {
"Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "" "Ignored packages only apply to the built-in updater. Your custom command controls its own exclusions.": "Ignorierte Pakete gelten nur für den integrierten Updater. Ihr benutzerdefinierter Befehl steuert seine eigenen Ausschlüsse."
}, },
"Image": { "Image": {
"Image": "Bild" "Image": "Bild"
@@ -4347,7 +4347,7 @@
"Invalid configuration": "Invalide Konfiguration" "Invalid configuration": "Invalide Konfiguration"
}, },
"Invalid package name — letters, digits and @._+:- only.": { "Invalid package name — letters, digits and @._+:- only.": {
"Invalid package name — letters, digits and @._+:- only.": "" "Invalid package name — letters, digits and @._+:- only.": "Ungültiger Paketname — nur Buchstaben, Ziffern und @._+:- zulässig."
}, },
"Invalid password for %1": { "Invalid password for %1": {
"Invalid password for %1": "Ungültiges Passwort für %1" "Invalid password for %1": "Ungültiges Passwort für %1"
@@ -5451,7 +5451,7 @@
"No output devices found": "Keine Ausgabegeräte gefunden" "No output devices found": "Keine Ausgabegeräte gefunden"
}, },
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": { "No packages ignored. Add one here or hover an update in the popout and click the hide button.": {
"No packages ignored. Add one here or hover an update in the popout and click the hide button.": "" "No packages ignored. Add one here or hover an update in the popout and click the hide button.": "Keine Pakete ignoriert. Fügen Sie hier eines hinzu oder bewegen Sie den Mauszeiger über ein Update im Popout und klicken Sie auf die Schaltfläche Ausblenden."
}, },
"No peers found": { "No peers found": {
"No peers found": "Keine Peers gefunden" "No peers found": "Keine Peers gefunden"
@@ -5937,7 +5937,7 @@
"PIN": "PIN" "PIN": "PIN"
}, },
"Package name (e.g., docker)": { "Package name (e.g., docker)": {
"Package name (e.g., docker)": "" "Package name (e.g., docker)": "Paketname (z. B. docker)"
}, },
"Pad": { "Pad": {
"Pad": "Auffüllen" "Pad": "Auffüllen"
@@ -7812,7 +7812,7 @@
"Status": "Status" "Status": "Status"
}, },
"Stop ignoring %1": { "Stop ignoring %1": {
"Stop ignoring %1": "" "Stop ignoring %1": "Ignorieren von %1 beenden"
}, },
"Stopped": { "Stopped": {
"Stopped": "Gestoppt" "Stopped": "Gestoppt"
@@ -8910,7 +8910,7 @@
"Votes": "Stimmen" "Votes": "Stimmen"
}, },
"W": { "W": {
"W": "" "W": "W"
}, },
"WPA/WPA2": { "WPA/WPA2": {
"WPA/WPA2": "WPA/WPA2" "WPA/WPA2": "WPA/WPA2"
@@ -9213,16 +9213,16 @@
"by %1": "von %1" "by %1": "von %1"
}, },
"checked %1d ago": { "checked %1d ago": {
"checked %1d ago": "" "checked %1d ago": "vor %1 T geprüft"
}, },
"checked %1h ago": { "checked %1h ago": {
"checked %1h ago": "" "checked %1h ago": "vor %1 Std. geprüft"
}, },
"checked %1m ago": { "checked %1m ago": {
"checked %1m ago": "" "checked %1m ago": "vor %1 Min. geprüft"
}, },
"checked just now": { "checked just now": {
"checked just now": "" "checked just now": "gerade eben geprüft"
}, },
"days": { "days": {
"days": "Tage" "days": "Tage"
@@ -4634,6 +4634,41 @@
"zenbrowser" "zenbrowser"
] ]
}, },
{
"section": "lockAuthSource",
"label": "Authentication Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
],
"icon": "key"
},
{
"section": "lockPamPath",
"label": "Authentication Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
]
},
{ {
"section": "lockScreenVideoCycling", "section": "lockScreenVideoCycling",
"label": "Automatic Cycling", "label": "Automatic Cycling",
@@ -7954,14 +7989,20 @@
"keywords": [ "keywords": [
"auth", "auth",
"authentication", "authentication",
"block",
"display manager", "display manager",
"fingerprint", "external",
"fprintd",
"greetd", "greetd",
"greeter", "greeter",
"login" "login",
"managed",
"pam",
"removes",
"stops",
"writing"
], ],
"icon": "fingerprint" "icon": "fingerprint",
"description": "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
}, },
{ {
"section": "greeterRememberLastSession", "section": "greeterRememberLastSession",
@@ -7999,6 +8040,28 @@
], ],
"description": "Pre-fill the last successful username on the greeter" "description": "Pre-fill the last successful username on the greeter"
}, },
{
"section": "greeterPamExternallyManaged",
"label": "greetd PAM is externally managed",
"tabIndex": 31,
"category": "Greeter",
"keywords": [
"auth",
"block",
"display manager",
"external",
"externally",
"greetd",
"greeter",
"login",
"managed",
"pam",
"removes",
"stops",
"writing"
],
"description": "DMS removes its managed block from /etc/pam.d/greetd and stops writing to it"
},
{ {
"section": "muxType", "section": "muxType",
"label": "Multiplexer", "label": "Multiplexer",
+17 -129
View File
@@ -1528,7 +1528,7 @@
{ {
"term": "Apply Changes", "term": "Apply Changes",
"translation": "", "translation": "",
"context": "", "context": "validate and apply custom PAM authentication source",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -1836,14 +1836,7 @@
{ {
"term": "Authentication changes apply automatically.", "term": "Authentication changes apply automatically.",
"translation": "", "translation": "",
"context": "", "context": "greeter auth setting description",
"reference": "",
"comment": ""
},
{
"term": "Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.",
"translation": "",
"context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -1913,7 +1906,7 @@
{ {
"term": "Auto", "term": "Auto",
"translation": "", "translation": "",
"context": "calendar backend option | theme category option", "context": "automatic PAM authentication source option | calendar backend option | theme category option",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -2281,13 +2274,6 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Available.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Awaiting fingerprint authentication", "term": "Awaiting fingerprint authentication",
"translation": "", "translation": "",
@@ -4993,7 +4979,7 @@
{ {
"term": "Custom...", "term": "Custom...",
"translation": "", "translation": "",
"context": "date format option", "context": "custom PAM authentication source option | date format option",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -6551,76 +6537,6 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Enabled, but fingerprint availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no fingerprint reader was detected.",
"translation": "",
"context": "",
"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": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but no registered security key was found yet. Register a key and run Sync.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled, but security-key availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM already provides fingerprint auth.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM already provides security-key auth.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Enabling WiFi...", "term": "Enabling WiFi...",
"translation": "", "translation": "",
@@ -7800,7 +7716,7 @@
{ {
"term": "Fingerprint availability could not be confirmed.", "term": "Fingerprint availability could not be confirmed.",
"translation": "", "translation": "",
"context": "", "context": "fingerprint setting status",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -7821,14 +7737,14 @@
{ {
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.", "term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.",
"translation": "", "translation": "",
"context": "", "context": "lock screen fingerprint setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.", "term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.",
"translation": "", "translation": "",
"context": "", "context": "greeter fingerprint login setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -12392,7 +12308,7 @@
{ {
"term": "No fingerprint reader detected.", "term": "No fingerprint reader detected.",
"translation": "", "translation": "",
"context": "", "context": "fingerprint setting status",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -12833,28 +12749,28 @@
{ {
"term": "Not available — install fprintd and pam_fprintd, or configure greetd PAM.", "term": "Not available — install fprintd and pam_fprintd, or configure greetd PAM.",
"translation": "", "translation": "",
"context": "", "context": "greeter fingerprint login setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install fprintd and pam_fprintd.", "term": "Not available — install fprintd and pam_fprintd.",
"translation": "", "translation": "",
"context": "", "context": "lock screen fingerprint setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install or configure pam_u2f, or configure greetd PAM.", "term": "Not available — install or configure pam_u2f, or configure greetd PAM.",
"translation": "", "translation": "",
"context": "", "context": "greeter security key login setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Not available — install or configure pam_u2f.", "term": "Not available — install or configure pam_u2f.",
"translation": "", "translation": "",
"context": "", "context": "lock screen security key setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -13166,13 +13082,6 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Only on Battery", "term": "Only on Battery",
"translation": "", "translation": "",
@@ -13603,35 +13512,14 @@
{ {
"term": "PAM already provides fingerprint auth. Enable this to show it at login.", "term": "PAM already provides fingerprint auth. Enable this to show it at login.",
"translation": "", "translation": "",
"context": "", "context": "greeter fingerprint login setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "PAM already provides security-key auth. Enable this to show it at login.", "term": "PAM already provides security-key auth. Enable this to show it at login.",
"translation": "", "translation": "",
"context": "", "context": "greeter security key login setting",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but availability could not be confirmed.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but no prints are enrolled yet.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "PAM provides fingerprint auth, but no reader was detected.",
"translation": "",
"context": "",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -16228,14 +16116,14 @@
{ {
"term": "Security-key availability could not be confirmed.", "term": "Security-key availability could not be confirmed.",
"translation": "", "translation": "",
"context": "", "context": "security key setting status",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{ {
"term": "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "term": "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.",
"translation": "", "translation": "",
"context": "", "context": "security key setting status",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
@@ -19987,7 +19875,7 @@
{ {
"term": "Use fingerprint authentication for the lock screen.", "term": "Use fingerprint authentication for the lock screen.",
"translation": "", "translation": "",
"context": "", "context": "lock screen fingerprint setting",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
+21 -4
View File
@@ -20,6 +20,7 @@ import re
import subprocess import subprocess
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
BOT_RE = re.compile(r"\[bot\]$|^github-actions$|^dependabot$", re.I) BOT_RE = re.compile(r"\[bot\]$|^github-actions$|^dependabot$", re.I)
# default blog-table exclusions # default blog-table exclusions
@@ -311,6 +312,13 @@ def main():
help="github format: omit heading and Full Changelog footer") help="github format: omit heading and Full Changelog footer")
ap.add_argument("--no-api", action="store_true", ap.add_argument("--no-api", action="store_true",
help="skip GitHub API, use git data only (degraded attribution)") help="skip GitHub API, use git data only (degraded attribution)")
ap.add_argument("-o", "--output", default=None, metavar="PATH",
help="write the output to PATH instead of stdout (parent dirs "
"are created)")
ap.add_argument("--version", default=None, metavar="X.Y.Z",
help="release version being cut; if set and -o/--output isn't, "
"writes to ~/Documents/dms-vX.Y.Z-changelog.md instead of "
"stdout")
args = ap.parse_args() args = ap.parse_args()
repo = args.repo repo = args.repo
@@ -329,16 +337,25 @@ def main():
drop = {x.lower() for x in excludes if x} drop = {x.lower() for x in excludes if x}
if args.format == "blog": if args.format == "blog":
# blog excludes from tables only; fixes list keeps everyone # blog excludes from tables only; fixes list keeps everyone
print(format_blog(repo, entries, args.range, exclude=drop)) text = format_blog(repo, entries, args.range, exclude=frozenset(drop))
return 0 else:
if drop: if drop:
entries = [e for e in entries entries = [e for e in entries
if (e["login"] or "").lower() not in drop if (e["login"] or "").lower() not in drop
and e["author_name"].lower() not in drop] and e["author_name"].lower() not in drop]
if args.format == "github": if args.format == "github":
print(format_github(repo, entries, args.range, bare=args.bare)) text = format_github(repo, entries, args.range, bare=args.bare)
else: else:
print(format_checklist(entries)) text = format_checklist(entries)
out_target = args.output or (f"~/Documents/dms-v{args.version}-changelog.md" if args.version else None)
if out_target:
out_path = Path(out_target).expanduser()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(text + "\n")
print(f"written to {out_path}", file=sys.stderr)
else:
print(text)
return 0 return 0