mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cc574ea5b | |||
| cdaedad969 | |||
| bf408f8d00 | |||
| b169fe0d77 | |||
| 9b67cedaa1 | |||
| 2edf70a144 | |||
| 72a71cacba | |||
| 0440e40a82 | |||
| ec27d4643c | |||
| f18d36f6c2 | |||
| 3254cc6a1e |
@@ -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 {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()...)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockEnabled = true;
|
||||||
loginSoundTimer.start();
|
loginSoundTimer.start();
|
||||||
osdStartupTimer.start();
|
osdStartupTimer.start();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+212
-308
File diff suppressed because it is too large
Load Diff
@@ -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": "أيام"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user