mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
32 Commits
31aeb8dc4b
...
v1.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f64bb8031 | |||
| eea7d12c0b | |||
| 85173126f4 | |||
| 222187d8a6 | |||
| bef3f65f63 | |||
| bff83fe563 | |||
| cbf00d133a | |||
| 347f06b758 | |||
| 9070903512 | |||
| e9d030f6d8 | |||
| fbf9e6d1b9 | |||
| e803812344 | |||
| 9a64f2acf0 | |||
| c647eafadc | |||
| 720ec07d13 | |||
| 4b4334e611 | |||
| b69a96e80b | |||
| 1e6a73fd60 | |||
| 60b6280750 | |||
| 9e079f8a4b | |||
| 62c2e858ef | |||
| 78357d45bb | |||
| 3ff9564c9b | |||
| b0989cecad | |||
| 47be6a1033 | |||
| 31b415b086 | |||
| 7156e1e299 | |||
| c72c9bfb08 | |||
| 73c75fcc2c | |||
| 2ff42eba41 | |||
| 9f13465cd7 | |||
| 366a98e0cc |
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
+1140
-156
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||||
|
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
immutablePolicyOnce sync.Once
|
||||||
|
immutablePolicy immutableCommandPolicy
|
||||||
|
immutablePolicyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/cli-policy.default.json
|
||||||
|
var defaultCLIPolicyJSON []byte
|
||||||
|
|
||||||
|
type immutableCommandPolicy struct {
|
||||||
|
ImmutableSystem bool
|
||||||
|
ImmutableReason string
|
||||||
|
BlockedCommands []string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliPolicyFile struct {
|
||||||
|
PolicyVersion int `json:"policy_version"`
|
||||||
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCommandSpec(raw string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||||
|
return strings.Join(strings.Fields(normalized), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBlockedCommands(raw []string) []string {
|
||||||
|
normalized := make([]string, 0, len(raw))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, cmd := range raw {
|
||||||
|
spec := normalizeCommandSpec(cmd)
|
||||||
|
if spec == "" || seen[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[spec] = true
|
||||||
|
normalized = append(normalized, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||||
|
normalizedPath := normalizeCommandSpec(commandPath)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range blocked {
|
||||||
|
spec := normalizeCommandSpec(entry)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(data, &policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||||
|
policyFile, err := loadPolicyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policyFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyFile.ImmutableSystem != nil {
|
||||||
|
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands != nil {
|
||||||
|
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
}
|
||||||
|
if policyFile.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*policyFile.Message)
|
||||||
|
if msg != "" {
|
||||||
|
base.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSReleaseMap(path string) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||||
|
values[key] = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyToken(text string, tokens ...string) bool {
|
||||||
|
if text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.Contains(text, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectImmutableSystem() (bool, string) {
|
||||||
|
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||||
|
return true, "/run/ostree-booted is present"
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := readOSReleaseMap("/etc/os-release")
|
||||||
|
if len(osRelease) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := osRelease["ID"]
|
||||||
|
idLike := osRelease["ID_LIKE"]
|
||||||
|
variantID := osRelease["VARIANT_ID"]
|
||||||
|
name := osRelease["NAME"]
|
||||||
|
prettyName := osRelease["PRETTY_NAME"]
|
||||||
|
|
||||||
|
immutableIDs := map[string]bool{
|
||||||
|
"bluefin": true,
|
||||||
|
"bazzite": true,
|
||||||
|
"silverblue": true,
|
||||||
|
"kinoite": true,
|
||||||
|
"sericea": true,
|
||||||
|
"onyx": true,
|
||||||
|
"aurora": true,
|
||||||
|
"fedora-iot": true,
|
||||||
|
"fedora-coreos": true,
|
||||||
|
}
|
||||||
|
if immutableIDs[id] {
|
||||||
|
return true, "os-release ID=" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||||
|
if hasAnyToken(variantID, markers...) {
|
||||||
|
return true, "os-release VARIANT_ID=" + variantID
|
||||||
|
}
|
||||||
|
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||||
|
return true, "os-release ID_LIKE=" + idLike
|
||||||
|
}
|
||||||
|
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||||
|
return true, "os-release identifies an atomic/ostree variant"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||||
|
immutablePolicyOnce.Do(func() {
|
||||||
|
detectedImmutable, reason := detectImmutableSystem()
|
||||||
|
immutablePolicy = immutableCommandPolicy{
|
||||||
|
ImmutableSystem: detectedImmutable,
|
||||||
|
ImmutableReason: reason,
|
||||||
|
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||||
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPolicy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||||
|
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if defaultPolicy.BlockedCommands != nil {
|
||||||
|
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||||
|
}
|
||||||
|
if defaultPolicy.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||||
|
if msg != "" {
|
||||||
|
immutablePolicy.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if immutablePolicyErr != nil {
|
||||||
|
return nil, immutablePolicyErr
|
||||||
|
}
|
||||||
|
return &immutablePolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||||
|
policy, err := getImmutablePolicy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !policy.ImmutableSystem {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||||
|
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := ""
|
||||||
|
if policy.ImmutableReason != "" {
|
||||||
|
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
|
}
|
||||||
+1
-10
@@ -16,19 +16,10 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to update
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|||||||
@@ -11,29 +11,20 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Block root
|
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
|
||||||
path, err := exec.LookPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
|
match app-id=r#"com.danklinux.dms$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -135,6 +135,42 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
||||||
|
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
||||||
|
data, err := os.ReadFile(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
var pkg string
|
||||||
|
var target *[]string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "makedepends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "makedepends = ")
|
||||||
|
target = &makedeps
|
||||||
|
case strings.HasPrefix(line, "depends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "depends = ")
|
||||||
|
target = &deps
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
||||||
|
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
||||||
|
pkg = pkg[:idx]
|
||||||
|
}
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg != "" {
|
||||||
|
*target = append(*target, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps, makedeps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||||
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -524,6 +560,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
||||||
|
if visited[pkg] {
|
||||||
|
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
visited[pkg] = true
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -610,39 +656,61 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Classifying dependencies as system or AUR",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies from .SRCINFO
|
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||||
depFilter := ""
|
if err != nil {
|
||||||
if pkg == "dms-shell-git" {
|
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
seen := make(map[string]bool)
|
||||||
fmt.Sprintf(`
|
var systemPkgs []string
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
var aurPkgs []string
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, depFilter, sudoPassword))
|
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
if seen[dep] || a.packageInstalled(dep) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dep] = true
|
||||||
|
if a.isInSystemRepo(dep) {
|
||||||
|
systemPkgs = append(systemPkgs, dep)
|
||||||
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
if len(systemPkgs) > 0 {
|
||||||
fmt.Sprintf(`
|
progressChan <- InstallProgressMsg{
|
||||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
Phase: PhaseAURPackages,
|
||||||
if [ ! -z "$makedeps" ]; then
|
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||||
fi
|
IsComplete: false,
|
||||||
`, srcinfoPath, sudoPassword))
|
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
for _, aurDep := range aurPkgs {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
||||||
|
}
|
||||||
|
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
||||||
|
startProgress+0.35*(endProgress-startProgress),
|
||||||
|
startProgress+0.39*(endProgress-startProgress),
|
||||||
|
visited,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# AppArmor profile for dms-greeter
|
||||||
|
#
|
||||||
|
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
||||||
|
# Manual edits will be overwritten on next sync.
|
||||||
|
#
|
||||||
|
# Mode: complain (denials are logged, nothing is blocked)
|
||||||
|
# To switch to enforce after validating with `aa-logprof`:
|
||||||
|
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
||||||
|
#
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
#include <abstractions/bash>
|
||||||
|
|
||||||
|
# The launcher script itself
|
||||||
|
/usr/bin/dms-greeter r,
|
||||||
|
|
||||||
|
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
||||||
|
/var/cache/dms-greeter/ rw,
|
||||||
|
/var/cache/dms-greeter/** rwlk,
|
||||||
|
|
||||||
|
# DMS config — packaged path
|
||||||
|
/usr/share/quickshell/dms-greeter/ r,
|
||||||
|
/usr/share/quickshell/dms-greeter/** r,
|
||||||
|
/usr/share/quickshell/ r,
|
||||||
|
/usr/share/quickshell/** r,
|
||||||
|
|
||||||
|
# DMS config — system and user overrides
|
||||||
|
/etc/dms/ r,
|
||||||
|
/etc/dms/** r,
|
||||||
|
/usr/share/dms/ r,
|
||||||
|
/usr/share/dms/** r,
|
||||||
|
/home/*/.config/quickshell/ r,
|
||||||
|
/home/*/.config/quickshell/** r,
|
||||||
|
/root/.config/quickshell/ r,
|
||||||
|
/root/.config/quickshell/** r,
|
||||||
|
|
||||||
|
# greetd / PAM — read-only for session setup
|
||||||
|
/etc/greetd/ r,
|
||||||
|
/etc/greetd/** r,
|
||||||
|
/etc/pam.d/ r,
|
||||||
|
/etc/pam.d/** r,
|
||||||
|
/usr/lib/pam.d/ r,
|
||||||
|
/usr/lib/pam.d/** r,
|
||||||
|
|
||||||
|
# Compositor binaries — run unconfined so each compositor uses its own profile
|
||||||
|
/usr/bin/niri Ux,
|
||||||
|
/usr/bin/hyprland Ux,
|
||||||
|
/usr/bin/Hyprland Ux,
|
||||||
|
/usr/bin/sway Ux,
|
||||||
|
/usr/bin/labwc Ux,
|
||||||
|
/usr/bin/scroll Ux,
|
||||||
|
/usr/bin/miracle-wm Ux,
|
||||||
|
/usr/bin/mango Ux,
|
||||||
|
|
||||||
|
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
||||||
|
/usr/bin/qs Ux,
|
||||||
|
/usr/bin/quickshell Ux,
|
||||||
|
|
||||||
|
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
||||||
|
/run/user/[0-9]*/ rw,
|
||||||
|
/run/user/[0-9]*/** rw,
|
||||||
|
|
||||||
|
# DRM / GPU devices (required for Wayland compositor startup)
|
||||||
|
/dev/dri/ r,
|
||||||
|
/dev/dri/* rw,
|
||||||
|
/dev/udmabuf rw,
|
||||||
|
|
||||||
|
# Input devices
|
||||||
|
/dev/input/ r,
|
||||||
|
/dev/input/* r,
|
||||||
|
|
||||||
|
# Systemd journal / logging
|
||||||
|
/run/systemd/journal/socket rw,
|
||||||
|
/dev/log rw,
|
||||||
|
|
||||||
|
# Shell helper binaries invoked by the launcher script
|
||||||
|
/usr/bin/env ix,
|
||||||
|
/usr/bin/mkdir ix,
|
||||||
|
/usr/bin/cat ix,
|
||||||
|
/usr/bin/grep ix,
|
||||||
|
/usr/bin/dirname ix,
|
||||||
|
/usr/bin/basename ix,
|
||||||
|
/usr/bin/command ix,
|
||||||
|
/bin/env ix,
|
||||||
|
/bin/mkdir ix,
|
||||||
|
|
||||||
|
# Signal management (compositor lifecycle)
|
||||||
|
signal (send, receive) set=("term", "int", "hup", "kill"),
|
||||||
|
}
|
||||||
+1356
-117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
|||||||
|
package greeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTestJSON(t *testing.T, path string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingsJSON string
|
||||||
|
sessionJSON string
|
||||||
|
wantSourcePath string
|
||||||
|
wantResolvedWallpaper string
|
||||||
|
wantDynamicOverrideUsed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||||
|
"matugenScheme": "scheme-tonal-spot",
|
||||||
|
"iconTheme": "Papirus"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":true}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||||
|
wantDynamicOverrideUsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic theme without override uses desktop colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": ""
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "purple",
|
||||||
|
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||||
|
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||||
|
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||||
|
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||||
|
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||||
|
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,7 @@ type Options struct {
|
|||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -274,6 +275,10 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -331,6 +336,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -597,10 +606,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||||
|
t.Skipf("system D-Bus unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ let
|
|||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
compositorPackage
|
compositorPackage
|
||||||
|
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -179,7 +180,9 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
||||||
|
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
||||||
|
cp "$theme_file" custom-theme.json
|
||||||
mv settings.json settings.orig.json
|
mv settings.json settings.orig.json
|
||||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -12,6 +12,27 @@ Singleton {
|
|||||||
signal popoutOpening
|
signal popoutOpening
|
||||||
signal popoutChanged
|
signal popoutChanged
|
||||||
|
|
||||||
|
function _closePopout(popout) {
|
||||||
|
switch (true) {
|
||||||
|
case popout.dashVisible !== undefined:
|
||||||
|
popout.dashVisible = false;
|
||||||
|
return;
|
||||||
|
case popout.notificationHistoryVisible !== undefined:
|
||||||
|
popout.notificationHistoryVisible = false;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
popout.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isStale(popout) {
|
||||||
|
try {
|
||||||
|
return !popout || !("shouldBeVisible" in popout);
|
||||||
|
} catch (e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showPopout(popout) {
|
function showPopout(popout) {
|
||||||
if (!popout || !popout.screen)
|
if (!popout || !popout.screen)
|
||||||
return;
|
return;
|
||||||
@@ -23,13 +44,11 @@ Singleton {
|
|||||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||||
if (!otherPopout || otherPopout === popout)
|
if (!otherPopout || otherPopout === popout)
|
||||||
continue;
|
continue;
|
||||||
if (otherPopout.dashVisible !== undefined) {
|
if (_isStale(otherPopout)) {
|
||||||
otherPopout.dashVisible = false;
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
continue;
|
||||||
otherPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
otherPopout.close();
|
|
||||||
}
|
}
|
||||||
|
_closePopout(otherPopout);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPopoutsByScreen[screenName] = popout;
|
currentPopoutsByScreen[screenName] = popout;
|
||||||
@@ -51,15 +70,9 @@ Singleton {
|
|||||||
function closeAllPopouts() {
|
function closeAllPopouts() {
|
||||||
for (const screenName in currentPopoutsByScreen) {
|
for (const screenName in currentPopoutsByScreen) {
|
||||||
const popout = currentPopoutsByScreen[screenName];
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
if (!popout)
|
if (!popout || _isStale(popout))
|
||||||
continue;
|
continue;
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentPopoutsByScreen = {};
|
currentPopoutsByScreen = {};
|
||||||
}
|
}
|
||||||
@@ -90,6 +103,12 @@ Singleton {
|
|||||||
if (!otherPopout)
|
if (!otherPopout)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (_isStale(otherPopout)) {
|
||||||
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
|
currentPopoutTriggers[otherScreenName] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (otherPopout === popout) {
|
if (otherPopout === popout) {
|
||||||
movedFromOtherScreen = true;
|
movedFromOtherScreen = true;
|
||||||
currentPopoutsByScreen[otherScreenName] = null;
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
@@ -97,45 +116,26 @@ Singleton {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherPopout.dashVisible !== undefined) {
|
_closePopout(otherPopout);
|
||||||
otherPopout.dashVisible = false;
|
|
||||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
|
||||||
otherPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
otherPopout.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout && currentPopout !== popout) {
|
if (currentPopout && currentPopout !== popout) {
|
||||||
if (currentPopout.dashVisible !== undefined) {
|
if (_isStale(currentPopout)) {
|
||||||
currentPopout.dashVisible = false;
|
currentPopoutsByScreen[screenName] = null;
|
||||||
} else if (currentPopout.notificationHistoryVisible !== undefined) {
|
currentPopoutTriggers[screenName] = null;
|
||||||
currentPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
} else {
|
||||||
currentPopout.close();
|
_closePopout(currentPopout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerId === undefined) {
|
if (triggerId === undefined) {
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1204,7 +1204,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -294,6 +294,17 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
|
property bool greeterRememberLastSession: true
|
||||||
|
property bool greeterRememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -495,6 +506,23 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
|
property bool lockFingerprintCanEnable: false
|
||||||
|
property bool lockFingerprintReady: false
|
||||||
|
property string lockFingerprintReason: "probe_failed"
|
||||||
|
property bool greeterFingerprintCanEnable: false
|
||||||
|
property bool greeterFingerprintReady: false
|
||||||
|
property string greeterFingerprintReason: "probe_failed"
|
||||||
|
property string greeterFingerprintSource: "none"
|
||||||
|
property bool enableU2f: false
|
||||||
|
property string u2fMode: "or"
|
||||||
|
property bool u2fAvailable: false
|
||||||
|
property bool lockU2fCanEnable: false
|
||||||
|
property bool lockU2fReady: false
|
||||||
|
property string lockU2fReason: "probe_failed"
|
||||||
|
property bool greeterU2fCanEnable: false
|
||||||
|
property bool greeterU2fReady: false
|
||||||
|
property string greeterU2fReason: "probe_failed"
|
||||||
|
property string greeterU2fSource: "none"
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
@@ -977,12 +1005,19 @@ Singleton {
|
|||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
|
function refreshAuthAvailability() {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
Processes.settingsRoot = root;
|
||||||
|
Processes.detectAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
Processes.detectFprintd();
|
refreshAuthAvailability();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -858,7 +858,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.fontFamily;
|
return GreetdSettings.getEffectiveFontFamily();
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1022,7 +1022,11 @@ Singleton {
|
|||||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||||
const defaults = themeData.variants.defaults || {};
|
const defaults = themeData.variants.defaults || {};
|
||||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||||
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const stored = isGreeterMode ?
|
||||||
|
(GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) :
|
||||||
|
(typeof SettingsData !== "undefined" ?
|
||||||
|
SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
||||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||||
const accentId = stored.accent || modeDefaults.accent || "";
|
const accentId = stored.accent || modeDefaults.accent || "";
|
||||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||||
@@ -1048,7 +1052,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode
|
||||||
|
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
||||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||||
@@ -1420,8 +1427,13 @@ Singleton {
|
|||||||
const defaults = customThemeRawData.variants.defaults || {};
|
const defaults = customThemeRawData.variants.defaults || {};
|
||||||
const darkDefaults = defaults.dark || {};
|
const darkDefaults = defaults.dark || {};
|
||||||
const lightDefaults = defaults.light || defaults.dark || {};
|
const lightDefaults = defaults.light || defaults.dark || {};
|
||||||
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
const storedDark = isGreeterMode
|
||||||
|
? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
||||||
|
const storedLight = isGreeterMode
|
||||||
|
? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
@@ -1439,7 +1451,10 @@ Singleton {
|
|||||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode
|
||||||
|
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
||||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||||
@@ -1763,10 +1778,11 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
blockLoading: false
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: !SessionData.isGreeterMode
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
|
|||||||
@@ -10,18 +10,352 @@ Singleton {
|
|||||||
|
|
||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
|
property string greetdPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
property string systemLoginPamText: ""
|
||||||
|
property string systemLocalLoginPamText: ""
|
||||||
|
property string commonAuthPcPamText: ""
|
||||||
|
property string loginPamText: ""
|
||||||
|
property string dankshellU2fPamText: ""
|
||||||
|
property string u2fKeysText: ""
|
||||||
|
|
||||||
|
property string fingerprintProbeOutput: ""
|
||||||
|
property int fingerprintProbeExitCode: 0
|
||||||
|
property bool fingerprintProbeStreamFinished: false
|
||||||
|
property bool fingerprintProbeExited: false
|
||||||
|
property string fingerprintProbeState: "probe_failed"
|
||||||
|
|
||||||
|
property string pamSupportProbeOutput: ""
|
||||||
|
property bool pamSupportProbeStreamFinished: false
|
||||||
|
property bool pamSupportProbeExited: false
|
||||||
|
property int pamSupportProbeExitCode: 0
|
||||||
|
property bool pamFprintSupportDetected: false
|
||||||
|
property bool pamU2fSupportDetected: false
|
||||||
|
|
||||||
|
readonly property string homeDir: Quickshell.env("HOME") || ""
|
||||||
|
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
||||||
|
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
|
||||||
|
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
|
||||||
|
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
|
||||||
|
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||||
|
|
||||||
|
function envFlag(name) {
|
||||||
|
const value = (Quickshell.env(name) || "").trim().toLowerCase();
|
||||||
|
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
||||||
|
return true;
|
||||||
|
if (value === "0" || value === "false" || value === "no" || value === "off")
|
||||||
|
return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||||
|
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||||
|
|
||||||
function detectQtTools() {
|
function detectQtTools() {
|
||||||
qtToolsDetectionProcess.running = true;
|
qtToolsDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectAuthCapabilities() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (forcedFprintAvailable === null) {
|
||||||
|
fingerprintProbeOutput = "";
|
||||||
|
fingerprintProbeStreamFinished = false;
|
||||||
|
fingerprintProbeExited = false;
|
||||||
|
fingerprintProbeProcess.running = true;
|
||||||
|
} else {
|
||||||
|
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
|
||||||
|
pamFprintSupportDetected = false;
|
||||||
|
pamU2fSupportDetected = false;
|
||||||
|
pamSupportProbeOutput = "";
|
||||||
|
pamSupportProbeStreamFinished = false;
|
||||||
|
pamSupportProbeExited = false;
|
||||||
|
pamSupportDetectionProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
fprintdDetectionProcess.running = true;
|
detectAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectU2f() {
|
||||||
|
detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamTextIncludesFile(pamText, filename) {
|
||||||
|
if (!pamText || !filename)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function greeterPamStackHasModule(moduleName) {
|
||||||
|
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||||
|
return true;
|
||||||
|
const includedPamStacks = [
|
||||||
|
["system-auth", systemAuthPamText],
|
||||||
|
["common-auth", commonAuthPamText],
|
||||||
|
["password-auth", passwordAuthPamText],
|
||||||
|
["system-login", systemLoginPamText],
|
||||||
|
["system-local-login", systemLocalLoginPamText],
|
||||||
|
["common-auth-pc", commonAuthPcPamText],
|
||||||
|
["login", loginPamText]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||||
|
const stack = includedPamStacks[i];
|
||||||
|
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEnrolledFingerprintOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
||||||
|
return true;
|
||||||
|
const lines = lower.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const trimmed = lines[i].trim();
|
||||||
|
if (trimmed.startsWith("finger:"))
|
||||||
|
return true;
|
||||||
|
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMissingFingerprintEnrollmentOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
return lower.includes("no fingers enrolled")
|
||||||
|
|| lower.includes("no fingerprints enrolled")
|
||||||
|
|| lower.includes("no prints enrolled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMissingFingerprintReaderOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
return lower.includes("no devices available")
|
||||||
|
|| lower.includes("no device available")
|
||||||
|
|| lower.includes("no devices found")
|
||||||
|
|| lower.includes("list_devices failed")
|
||||||
|
|| lower.includes("no device");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFingerprintProbe(exitCode, output) {
|
||||||
|
if (hasEnrolledFingerprintOutput(output))
|
||||||
|
return "ready";
|
||||||
|
if (hasMissingFingerprintEnrollmentOutput(output))
|
||||||
|
return "missing_enrollment";
|
||||||
|
if (hasMissingFingerprintReaderOutput(output))
|
||||||
|
return "missing_reader";
|
||||||
|
if (exitCode === 0)
|
||||||
|
return "missing_enrollment";
|
||||||
|
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
||||||
|
return "probe_failed";
|
||||||
|
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockFingerprintCapability(canEnable, ready, reason) {
|
||||||
|
settingsRoot.lockFingerprintCanEnable = canEnable;
|
||||||
|
settingsRoot.lockFingerprintReady = ready;
|
||||||
|
settingsRoot.lockFingerprintReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockU2fCapability(canEnable, ready, reason) {
|
||||||
|
settingsRoot.lockU2fCanEnable = canEnable;
|
||||||
|
settingsRoot.lockU2fReady = ready;
|
||||||
|
settingsRoot.lockU2fReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
|
||||||
|
settingsRoot.greeterFingerprintCanEnable = canEnable;
|
||||||
|
settingsRoot.greeterFingerprintReady = ready;
|
||||||
|
settingsRoot.greeterFingerprintReason = reason;
|
||||||
|
settingsRoot.greeterFingerprintSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGreeterU2fCapability(canEnable, ready, reason, source) {
|
||||||
|
settingsRoot.greeterU2fCanEnable = canEnable;
|
||||||
|
settingsRoot.greeterU2fReady = ready;
|
||||||
|
settingsRoot.greeterU2fReason = reason;
|
||||||
|
settingsRoot.greeterU2fSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeFingerprintCapabilities() {
|
||||||
|
if (forcedFprintAvailable !== null) {
|
||||||
|
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
const source = forcedFprintAvailable ? "dms" : "none";
|
||||||
|
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
|
||||||
|
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = fingerprintProbeState;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setLockFingerprintCapability(true, true, "ready");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setLockFingerprintCapability(true, false, "missing_enrollment");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setLockFingerprintCapability(false, false, "missing_reader");
|
||||||
|
break;
|
||||||
|
case "missing_pam_support":
|
||||||
|
setLockFingerprintCapability(false, false, "missing_pam_support");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setLockFingerprintCapability(false, false, "probe_failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (greeterPamHasFprint) {
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setGreeterFingerprintCapability(true, true, "ready", "dms");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
|
||||||
|
break;
|
||||||
|
case "missing_pam_support":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeU2fCapabilities() {
|
||||||
|
if (forcedU2fAvailable !== null) {
|
||||||
|
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
|
||||||
|
const source = forcedU2fAvailable ? "dms" : "none";
|
||||||
|
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
|
||||||
|
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
||||||
|
const lockCanEnable = lockReady || pamU2fSupportDetected;
|
||||||
|
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||||
|
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
|
||||||
|
|
||||||
|
if (greeterPamHasU2f) {
|
||||||
|
setGreeterU2fCapability(true, true, "configured_externally", "pam");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const greeterReady = homeU2fKeysDetected;
|
||||||
|
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
|
||||||
|
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||||
|
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeAuthCapabilities() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
recomputeFingerprintCapabilities();
|
||||||
|
recomputeU2fCapabilities();
|
||||||
|
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
|
||||||
|
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeFingerprintProbe() {
|
||||||
|
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
|
||||||
|
return;
|
||||||
|
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizePamSupportProbe() {
|
||||||
|
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pamFprintSupportDetected = false;
|
||||||
|
pamU2fSupportDetected = false;
|
||||||
|
|
||||||
|
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const parts = lines[i].split(":");
|
||||||
|
if (parts.length !== 2)
|
||||||
|
continue;
|
||||||
|
if (parts[0] === "pam_fprintd.so")
|
||||||
|
pamFprintSupportDetected = parts[1] === "true";
|
||||||
|
else if (parts[0] === "pam_u2f.so")
|
||||||
|
pamU2fSupportDetected = parts[1] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
|
||||||
|
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||||
|
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
property var qtToolsDetectionProcess: Process {
|
property var qtToolsDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
||||||
running: false
|
running: false
|
||||||
@@ -31,15 +365,15 @@ Singleton {
|
|||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
var lines = text.trim().split('\n');
|
const lines = text.trim().split("\n");
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('qt5ct:')) {
|
if (line.startsWith("qt5ct:")) {
|
||||||
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
||||||
} else if (line.startsWith('qt6ct:')) {
|
} else if (line.startsWith("qt6ct:")) {
|
||||||
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
||||||
} else if (line.startsWith('gtk:')) {
|
} else if (line.startsWith("gtk:")) {
|
||||||
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +381,181 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fprintdDetectionProcess: Process {
|
property var fingerprintProbeProcess: Process {
|
||||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.fingerprintProbeOutput = text || "";
|
||||||
|
root.fingerprintProbeStreamFinished = true;
|
||||||
|
root.finalizeFingerprintProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
if (!settingsRoot)
|
root.fingerprintProbeExitCode = exitCode;
|
||||||
return;
|
root.fingerprintProbeExited = true;
|
||||||
settingsRoot.fprintdAvailable = (exitCode === 0);
|
root.finalizeFingerprintProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var pamSupportDetectionProcess: Process {
|
||||||
|
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.pamSupportProbeOutput = text || "";
|
||||||
|
root.pamSupportProbeStreamFinished = true;
|
||||||
|
root.finalizePamSupportProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
root.pamSupportProbeExitCode = exitCode;
|
||||||
|
root.pamSupportProbeExited = true;
|
||||||
|
root.finalizePamSupportProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLoginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLoginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLocalLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-local-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLocalLoginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLocalLoginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPcPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth-pc"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPcPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPcPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginPamWatcher
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.loginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.loginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: dankshellU2fPamWatcher
|
||||||
|
path: "/etc/pam.d/dankshell-u2f"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.dankshellU2fPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.dankshellU2fPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: u2fKeysWatcher
|
||||||
|
path: root.u2fKeysPath
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.u2fKeysText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.u2fKeysText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,17 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
|
greeterRememberLastSession: { def: true },
|
||||||
|
greeterRememberLastUser: { def: true },
|
||||||
|
greeterEnableFprint: { def: false },
|
||||||
|
greeterEnableU2f: { def: false },
|
||||||
|
greeterWallpaperPath: { def: "" },
|
||||||
|
greeterUse24HourClock: { def: true },
|
||||||
|
greeterShowSeconds: { def: false },
|
||||||
|
greeterPadHours12Hour: { def: false },
|
||||||
|
greeterLockDateFormat: { def: "" },
|
||||||
|
greeterFontFamily: { def: "" },
|
||||||
|
greeterWallpaperFillMode: { def: "" },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -318,6 +329,23 @@ var SPEC = {
|
|||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
|
lockFingerprintCanEnable: { def: false, persist: false },
|
||||||
|
lockFingerprintReady: { def: false, persist: false },
|
||||||
|
lockFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterFingerprintCanEnable: { def: false, persist: false },
|
||||||
|
greeterFingerprintReady: { def: false, persist: false },
|
||||||
|
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterFingerprintSource: { def: "none", persist: false },
|
||||||
|
enableU2f: { def: false },
|
||||||
|
u2fMode: { def: "or" },
|
||||||
|
u2fAvailable: { def: false, persist: false },
|
||||||
|
lockU2fCanEnable: { def: false, persist: false },
|
||||||
|
lockU2fReady: { def: false, persist: false },
|
||||||
|
lockU2fReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterU2fCanEnable: { def: false, persist: false },
|
||||||
|
greeterU2fReady: { def: false, persist: false },
|
||||||
|
greeterU2fReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterU2fSource: { def: "none", persist: false },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
|||||||
@@ -798,9 +798,8 @@ Item {
|
|||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Notepad {
|
Notepad {
|
||||||
onHideRequested: {
|
slideout: notepadSlideout
|
||||||
notepadSlideout.hide();
|
onHideRequested: notepadSlideout.hide()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
|||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property var currentFlow: PolkitService.agent?.flow
|
property var currentFlow: PolkitService.agent?.flow
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool awaitingFprintForPassword: false
|
||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
|
|
||||||
|
property string polkitEtcPamText: ""
|
||||||
|
property string polkitLibPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
readonly property bool polkitPamHasFprint: {
|
||||||
|
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||||
|
if (!polkitText)
|
||||||
|
return false;
|
||||||
|
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (line && line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function focusPasswordField() {
|
function focusPasswordField() {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
|||||||
function show() {
|
function show() {
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(focusPasswordField);
|
Qt.callLater(focusPasswordField);
|
||||||
}
|
}
|
||||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
|||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commitSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
|
currentFlow.submit(passwordInput);
|
||||||
|
passwordInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
function submitAuth() {
|
function submitAuth() {
|
||||||
if (!currentFlow || isLoading)
|
if (!currentFlow || isLoading)
|
||||||
return;
|
return;
|
||||||
isLoading = true;
|
if (!currentFlow.isResponseRequired) {
|
||||||
currentFlow.submit(passwordInput);
|
awaitingFprintForPassword = true;
|
||||||
passwordInput = "";
|
return;
|
||||||
|
}
|
||||||
|
_commitSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAuth() {
|
function cancelAuth() {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
currentFlow.cancelAuthenticationRequest();
|
currentFlow.cancelAuthenticationRequest();
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
|||||||
function onIsResponseRequiredChanged() {
|
function onIsResponseRequiredChanged() {
|
||||||
if (!currentFlow.isResponseRequired)
|
if (!currentFlow.isResponseRequired)
|
||||||
return;
|
return;
|
||||||
|
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||||
|
_commitSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
@@ -101,6 +156,41 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitEtcPamText = text()
|
||||||
|
onLoadFailed: root.polkitEtcPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/usr/lib/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitLibPamText = text()
|
||||||
|
onLoadFailed: root.polkitLibPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.systemAuthPamText = text()
|
||||||
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.commonAuthPamText = text()
|
||||||
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
|
|
||||||
@@ -205,36 +295,30 @@ FloatingWindow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: inputFieldHeight
|
height: inputFieldHeight
|
||||||
radius: Theme.cornerRadius
|
backgroundColor: Theme.surfaceHover
|
||||||
color: Theme.surfaceHover
|
normalBorderColor: Theme.outlineStrong
|
||||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
focusedBorderColor: Theme.primary
|
||||||
border.width: passwordField.activeFocus ? 2 : 1
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
|
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||||
|
leftIconSize: 20
|
||||||
|
leftIconColor: Theme.primary
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
opacity: isLoading ? 0.5 : 1
|
opacity: isLoading ? 0.5 : 1
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
MouseArea {
|
textColor: Theme.surfaceText
|
||||||
anchors.fill: parent
|
text: passwordInput
|
||||||
enabled: !isLoading
|
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||||
onClicked: passwordField.forceActiveFocus()
|
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||||
}
|
placeholderText: ""
|
||||||
|
enabled: !isLoading
|
||||||
DankTextField {
|
onTextEdited: passwordInput = text
|
||||||
id: passwordField
|
onAccepted: submitAuth()
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: passwordInput
|
|
||||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
|
||||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: !isLoading
|
|
||||||
onTextEdited: passwordInput = text
|
|
||||||
onAccepted: submitAuth()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -100,10 +100,10 @@ Variants {
|
|||||||
Connections {
|
Connections {
|
||||||
target: currentWallpaper
|
target: currentWallpaper
|
||||||
function onStatusChanged() {
|
function onStatusChanged() {
|
||||||
if (currentWallpaper.status === Image.Ready) {
|
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||||
root._renderSettling = true;
|
return;
|
||||||
renderSettleTimer.restart();
|
root._renderSettling = true;
|
||||||
}
|
renderSettleTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +206,7 @@ Variants {
|
|||||||
visible: false
|
visible: false
|
||||||
opacity: 1
|
opacity: 1
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -218,6 +219,7 @@ Variants {
|
|||||||
visible: false
|
visible: false
|
||||||
opacity: 0
|
opacity: 0
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -300,6 +302,8 @@ Variants {
|
|||||||
root.useNextForEffect = false;
|
root.useNextForEffect = false;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
function readBoolOverride(envReader, names, fallbackValue) {
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
const name = names[i];
|
||||||
|
const raw = envReader(name);
|
||||||
|
if (raw === undefined || raw === null || raw === "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const normalized = String(raw).trim().toLowerCase();
|
||||||
|
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
||||||
|
return true;
|
||||||
|
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
||||||
|
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
property string lastSessionId: ""
|
property string lastSessionId: ""
|
||||||
property string lastSuccessfulUser: ""
|
property string lastSuccessfulUser: ""
|
||||||
@@ -19,7 +22,6 @@ Singleton {
|
|||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
Quickshell.execDetached(["mkdir", "-p", greetCfgDir]);
|
|
||||||
loadMemory();
|
loadMemory();
|
||||||
loadSessionConfig();
|
loadSessionConfig();
|
||||||
}
|
}
|
||||||
@@ -49,26 +51,44 @@ Singleton {
|
|||||||
if (!content || !content.trim())
|
if (!content || !content.trim())
|
||||||
return;
|
return;
|
||||||
const memory = JSON.parse(content);
|
const memory = JSON.parse(content);
|
||||||
lastSessionId = memory.lastSessionId || "";
|
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||||
lastSuccessfulUser = memory.lastSuccessfulUser || "";
|
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||||
|
if (!rememberLastSession || !rememberLastUser)
|
||||||
|
saveMemory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd memory:", e);
|
console.warn("Failed to parse greetd memory:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMemory() {
|
function saveMemory() {
|
||||||
memoryFileView.setText(JSON.stringify({
|
let memory = {};
|
||||||
"lastSessionId": lastSessionId,
|
if (rememberLastSession && lastSessionId)
|
||||||
"lastSuccessfulUser": lastSuccessfulUser
|
memory.lastSessionId = lastSessionId;
|
||||||
}, null, 2));
|
if (rememberLastUser && lastSuccessfulUser)
|
||||||
|
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||||
|
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
function setLastSessionId(id) {
|
||||||
|
if (!rememberLastSession) {
|
||||||
|
if (lastSessionId !== "") {
|
||||||
|
lastSessionId = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSessionId = id || "";
|
lastSessionId = id || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
function setLastSuccessfulUser(username) {
|
||||||
|
if (!rememberLastUser) {
|
||||||
|
if (lastSuccessfulUser !== "") {
|
||||||
|
lastSuccessfulUser = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSuccessfulUser = username || "";
|
lastSuccessfulUser = username || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,22 +5,36 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string configPath: {
|
readonly property string configPath: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/settings.json";
|
return greetCfgDir + "/settings.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string _greeterCacheDir: {
|
||||||
|
const i = root.configPath.lastIndexOf("/");
|
||||||
|
return i >= 0 ? root.configPath.substring(0, i) : "";
|
||||||
|
}
|
||||||
|
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
|
||||||
|
|
||||||
property string currentThemeName: "purple"
|
property string currentThemeName: "purple"
|
||||||
property bool settingsLoaded: false
|
property bool settingsLoaded: false
|
||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
|
property var registryThemeVariants: ({})
|
||||||
property string matugenScheme: "scheme-tonal-spot"
|
property string matugenScheme: "scheme-tonal-spot"
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
@@ -41,6 +55,11 @@ Singleton {
|
|||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
|
property bool rememberLastSession: true
|
||||||
|
property bool rememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property real powerActionHoldDuration: 0.5
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
@@ -52,66 +71,105 @@ Singleton {
|
|||||||
|
|
||||||
function parseSettings(content) {
|
function parseSettings(content) {
|
||||||
try {
|
try {
|
||||||
|
let settings = {};
|
||||||
if (content && content.trim()) {
|
if (content && content.trim()) {
|
||||||
const settings = JSON.parse(content);
|
settings = JSON.parse(content);
|
||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
}
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
|
||||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
|
||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
|
||||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
|
||||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
|
||||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
|
||||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
|
||||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
|
||||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
|
||||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
|
||||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
|
||||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
|
||||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
|
||||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
|
||||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
|
||||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
|
||||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
|
||||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
|
||||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
|
||||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
|
||||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
|
||||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
|
||||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
|
||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
|
||||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
|
||||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
|
||||||
settingsLoaded = true;
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
||||||
if (currentThemeName === "custom" && customThemeFile) {
|
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
||||||
Theme.loadCustomThemeFromFile(customThemeFile);
|
|
||||||
}
|
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||||
Theme.applyGreeterTheme(currentThemeName);
|
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||||
|
registryThemeVariants = settings.registryThemeVariants !== undefined ?
|
||||||
|
settings.registryThemeVariants : ({});
|
||||||
|
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||||
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
|
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||||
|
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
|
||||||
|
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
|
||||||
|
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
|
||||||
|
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
||||||
|
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
||||||
|
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
||||||
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
|
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
||||||
|
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
||||||
|
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
||||||
|
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
||||||
|
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
||||||
|
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
||||||
|
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
||||||
|
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
||||||
|
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
||||||
|
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
||||||
|
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
||||||
|
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
||||||
|
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
||||||
|
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
||||||
|
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||||
|
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||||
|
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||||
|
if (envRememberLastSession !== undefined) {
|
||||||
|
rememberLastSession = envRememberLastSession;
|
||||||
|
} else {
|
||||||
|
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
||||||
|
}
|
||||||
|
if (envRememberLastUser !== undefined) {
|
||||||
|
rememberLastUser = envRememberLastUser;
|
||||||
|
} else {
|
||||||
|
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
||||||
|
}
|
||||||
|
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
||||||
|
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
||||||
|
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
||||||
|
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||||
|
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||||
|
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||||
|
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
||||||
|
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
||||||
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||||
|
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||||
|
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (currentThemeName === "custom" && customThemeFile) {
|
||||||
|
Theme.loadCustomThemeFromFile(customThemeFile);
|
||||||
}
|
}
|
||||||
|
Theme.applyGreeterTheme(currentThemeName);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd settings:", e);
|
console.warn("Failed to parse greetd settings:", e);
|
||||||
|
} finally {
|
||||||
|
settingsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock)
|
const use24 = greeterUse24HourClock;
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
const secs = greeterShowSeconds;
|
||||||
if (padHours12Hour)
|
const pad = greeterPadHours12Hour;
|
||||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
if (use24)
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
return secs ? "hh:mm:ss" : "hh:mm";
|
||||||
|
if (pad)
|
||||||
|
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
|
return secs ? "h:mm:ss AP" : "h:mm AP";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
function getEffectiveLockDateFormat() {
|
||||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
||||||
|
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveWallpaperFillMode() {
|
||||||
|
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveFontFamily() {
|
||||||
|
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
function getFilteredScreens(componentId) {
|
||||||
@@ -133,5 +191,9 @@ Singleton {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
console.warn("Failed to load greetd settings:", error);
|
||||||
|
root.parseSettings("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,38 @@ Item {
|
|||||||
signal launchRequested
|
signal launchRequested
|
||||||
|
|
||||||
property bool weatherInitialized: false
|
property bool weatherInitialized: false
|
||||||
|
property bool awaitingExternalAuth: false
|
||||||
|
property bool pendingPasswordResponse: false
|
||||||
|
property bool passwordSubmitRequested: false
|
||||||
|
property bool cancelingExternalAuthForPassword: false
|
||||||
|
property int defaultAuthTimeoutMs: 10000
|
||||||
|
property int externalAuthTimeoutMs: 30000
|
||||||
|
property int memoryFlushDelayMs: 120
|
||||||
|
property string pendingLaunchCommand: ""
|
||||||
|
property var pendingLaunchEnv: []
|
||||||
|
property int passwordFailureCount: 0
|
||||||
|
property int passwordAttemptLimitHint: 0
|
||||||
|
property string authFeedbackMessage: ""
|
||||||
|
property string greetdPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
property string systemLoginPamText: ""
|
||||||
|
property string systemLocalLoginPamText: ""
|
||||||
|
property string commonAuthPcPamText: ""
|
||||||
|
property string loginPamText: ""
|
||||||
|
property string faillockConfigText: ""
|
||||||
|
property bool greeterWallpaperOverrideExists: false
|
||||||
|
property string externalAuthAutoStartedForUser: ""
|
||||||
|
property int passwordSessionTransitionRetryCount: 0
|
||||||
|
property int maxPasswordSessionTransitionRetries: 2
|
||||||
|
property bool fprintdProbeComplete: false
|
||||||
|
property bool fprintdHasDevice: false
|
||||||
|
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
|
||||||
|
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
|
||||||
|
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||||
|
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||||
|
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||||
|
|
||||||
function initWeatherService() {
|
function initWeatherService() {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
@@ -44,34 +76,492 @@ Item {
|
|||||||
WeatherService.forceRefresh();
|
WeatherService.forceRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamTextIncludesFile(pamText, filename) {
|
||||||
|
if (!pamText || !filename)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function greeterPamStackHasModule(moduleName) {
|
||||||
|
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||||
|
return true;
|
||||||
|
const includedPamStacks = [
|
||||||
|
["system-auth", systemAuthPamText],
|
||||||
|
["common-auth", commonAuthPamText],
|
||||||
|
["password-auth", passwordAuthPamText],
|
||||||
|
["system-login", systemLoginPamText],
|
||||||
|
["system-local-login", systemLocalLoginPamText],
|
||||||
|
["common-auth-pc", commonAuthPcPamText],
|
||||||
|
["login", loginPamText]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||||
|
const stack = includedPamStacks[i];
|
||||||
|
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usesPamLockoutPolicy(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePamLineDenyValue(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return -1;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFaillockDenyValue(configText) {
|
||||||
|
if (!configText)
|
||||||
|
return -1;
|
||||||
|
const lines = configText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPasswordAttemptPolicyHint() {
|
||||||
|
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
|
||||||
|
let lockoutConfigured = false;
|
||||||
|
let denyFromPam = -1;
|
||||||
|
for (let i = 0; i < pamSources.length; i++) {
|
||||||
|
const source = pamSources[i];
|
||||||
|
if (!source)
|
||||||
|
continue;
|
||||||
|
if (usesPamLockoutPolicy(source))
|
||||||
|
lockoutConfigured = true;
|
||||||
|
const denyValue = parsePamLineDenyValue(source);
|
||||||
|
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
|
||||||
|
denyFromPam = denyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockoutConfigured) {
|
||||||
|
passwordAttemptLimitHint = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
|
||||||
|
if (denyFromConfig >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromConfig;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denyFromPam >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromPam;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_faillock default deny value when no explicit config is set.
|
||||||
|
passwordAttemptLimitHint = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyLockoutMessage(message) {
|
||||||
|
const lower = (message || "").toLowerCase();
|
||||||
|
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAuthMessage() {
|
||||||
|
if (GreeterState.pamState === "error")
|
||||||
|
return "Authentication error - try again";
|
||||||
|
if (GreeterState.pamState === "max")
|
||||||
|
return "Too many failed attempts - account may be locked";
|
||||||
|
if (GreeterState.pamState === "fail") {
|
||||||
|
if (passwordAttemptLimitHint > 0) {
|
||||||
|
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
|
||||||
|
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
|
||||||
|
}
|
||||||
|
return "Incorrect password - next failures may trigger account lockout";
|
||||||
|
}
|
||||||
|
return "Incorrect password";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthFeedback() {
|
||||||
|
GreeterState.pamState = "";
|
||||||
|
authFeedbackMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPasswordSessionTransition(clearSubmitRequest) {
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
passwordSessionTransitionRetryCount = 0;
|
||||||
|
if (clearSubmitRequest)
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GreetdSettings
|
target: GreetdSettings
|
||||||
function onSettingsLoadedChanged() {
|
function onSettingsLoadedChanged() {
|
||||||
if (GreetdSettings.settingsLoaded)
|
if (GreetdSettings.settingsLoaded) {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
if (isPrimaryScreen) {
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastUserChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastSessionChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLoginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLoginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLocalLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-local-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLocalLoginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLocalLoginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPcPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth-pc"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPcPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPcPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginPamWatcher
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.loginPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.loginPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: faillockConfigWatcher
|
||||||
|
path: "/etc/security/faillock.conf"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.faillockConfigText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.faillockConfigText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
refreshPasswordAttemptPolicyHint();
|
||||||
|
|
||||||
if (isPrimaryScreen)
|
if (isPrimaryScreen)
|
||||||
applyLastSuccessfulUser();
|
applyLastSuccessfulUser();
|
||||||
|
|
||||||
if (CompositorService.isHyprland)
|
if (CompositorService.isHyprland)
|
||||||
updateHyprlandLayout();
|
updateHyprlandLayout();
|
||||||
|
|
||||||
|
fprintdDeviceProbe.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLastSuccessfulUser() {
|
function applyLastSuccessfulUser() {
|
||||||
|
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||||
|
return;
|
||||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||||
GreeterState.username = lastUser;
|
GreeterState.username = lastUser;
|
||||||
GreeterState.usernameInput = lastUser;
|
GreeterState.usernameInput = lastUser;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(lastUser);
|
PortalService.getGreeterUserProfileImage(lastUser);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitUsername(rawValue) {
|
||||||
|
const user = (rawValue || "").trim();
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
if (GreeterState.username !== user) {
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
|
externalAuthAutoStartedForUser = "";
|
||||||
|
}
|
||||||
|
GreeterState.username = user;
|
||||||
|
GreeterState.showPasswordInput = true;
|
||||||
|
PortalService.getGreeterUserProfileImage(user);
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBufferedPassword() {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
|
||||||
|
Greetd.respond(GreeterState.passwordBuffer || "");
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPasswordSessionTransition() {
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && !hasPasswordBuffer)
|
||||||
|
return;
|
||||||
|
if (cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelingExternalAuthForPassword = true;
|
||||||
|
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAuthSession(submitPassword) {
|
||||||
|
submitPassword = submitPassword === true;
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking)
|
||||||
|
return;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (Greetd.state !== GreetdState.Inactive) {
|
||||||
|
if (pendingPasswordResponse && submitPassword)
|
||||||
|
submitBufferedPassword();
|
||||||
|
else if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cancelingExternalAuthForPassword) {
|
||||||
|
if (submitPassword)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = submitPassword;
|
||||||
|
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
|
||||||
|
// Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
|
||||||
|
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
|
||||||
|
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.createSession(GreeterState.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAutoStartExternalAuth() {
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (!root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
|
||||||
|
return;
|
||||||
|
if (externalAuthAutoStartedForUser === GreeterState.username)
|
||||||
|
return;
|
||||||
|
|
||||||
|
externalAuthAutoStartedForUser = GreeterState.username;
|
||||||
|
startAuthSession(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalAuthPrompt(message, responseRequired) {
|
||||||
|
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
||||||
|
return !responseRequired;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
WeatherService.removeRef();
|
WeatherService.removeRef();
|
||||||
@@ -113,6 +603,34 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
|
||||||
|
Process {
|
||||||
|
id: fprintdDeviceProbe
|
||||||
|
running: false
|
||||||
|
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
|
||||||
|
// keeping the PAM-only fallback active in those cases.
|
||||||
|
command: ["sh", "-c",
|
||||||
|
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
|
||||||
|
"gdbus call --system " +
|
||||||
|
"--dest net.reactivated.Fprint " +
|
||||||
|
"--object-path /net/reactivated/Fprint/Manager " +
|
||||||
|
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
|
||||||
|
"|| echo PROBE_UNAVAILABLE"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.includes("PROBE_UNAVAILABLE"))
|
||||||
|
return; // PAM-only fallback stays active
|
||||||
|
root.fprintdHasDevice = text.includes("objectpath");
|
||||||
|
root.fprintdProbeComplete = true;
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onExited: function(exitCode, exitStatus) {
|
||||||
|
if (!root.fprintdProbeComplete)
|
||||||
|
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CompositorService.isHyprland ? Hyprland : null
|
target: CompositorService.isHyprland ? Hyprland : null
|
||||||
enabled: CompositorService.isHyprland
|
enabled: CompositorService.isHyprland
|
||||||
@@ -143,10 +661,39 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greeterWallpaperOverrideFile
|
||||||
|
path: GreetdSettings.greeterWallpaperOverridePath
|
||||||
|
printErrors: false
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: root.greeterWallpaperOverrideExists = true
|
||||||
|
onLoadFailed: root.greeterWallpaperOverrideExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GreetdSettings
|
||||||
|
function onGreeterWallpaperOverridePathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperOverridePath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
function onGreeterWallpaperPathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperPath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankBackdrop {
|
DankBackdrop {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
visible: {
|
visible: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return false;
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
@@ -159,12 +706,14 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
||||||
}
|
}
|
||||||
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
|
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
cache: true
|
cache: true
|
||||||
@@ -327,10 +876,7 @@ Item {
|
|||||||
anchors.top: clockContainer.bottom
|
anchors.top: clockContainer.bottom
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
text: {
|
text: {
|
||||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
|
|
||||||
}
|
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -399,6 +945,9 @@ Item {
|
|||||||
if (GreeterState.showPasswordInput && revealButton.visible) {
|
if (GreeterState.showPasswordInput && revealButton.visible) {
|
||||||
margin += revealButton.width;
|
margin += revealButton.width;
|
||||||
}
|
}
|
||||||
|
if (externalAuthButton.visible) {
|
||||||
|
margin += externalAuthButton.width;
|
||||||
|
}
|
||||||
if (virtualKeyboardButton.visible) {
|
if (virtualKeyboardButton.visible) {
|
||||||
margin += virtualKeyboardButton.width;
|
margin += virtualKeyboardButton.width;
|
||||||
}
|
}
|
||||||
@@ -415,21 +964,18 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
GreeterState.passwordBuffer = text;
|
GreeterState.passwordBuffer = text;
|
||||||
|
if (!text || text.length === 0)
|
||||||
|
root.passwordSubmitRequested = false;
|
||||||
} else {
|
} else {
|
||||||
GreeterState.usernameInput = text;
|
GreeterState.usernameInput = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
GreeterState.username = text.trim();
|
root.submitUsername(text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
syncingFromState = true;
|
syncingFromState = true;
|
||||||
text = "";
|
text = "";
|
||||||
syncingFromState = false;
|
syncingFromState = false;
|
||||||
@@ -461,14 +1007,14 @@ Item {
|
|||||||
|
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
if (GreeterState.unlocking) {
|
if (GreeterState.unlocking) {
|
||||||
return "Logging in...";
|
return "Logging in...";
|
||||||
}
|
}
|
||||||
if (Greetd.state !== GreetdState.Inactive) {
|
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
@@ -476,7 +1022,7 @@ Item {
|
|||||||
}
|
}
|
||||||
return "Username...";
|
return "Username...";
|
||||||
}
|
}
|
||||||
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
|
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||||
|
|
||||||
@@ -498,7 +1044,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -528,15 +1074,27 @@ Item {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: revealButton
|
id: revealButton
|
||||||
|
|
||||||
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: parent.showPassword = !parent.showPassword
|
onClicked: parent.showPassword = !parent.showPassword
|
||||||
}
|
}
|
||||||
|
DankActionButton {
|
||||||
|
id: externalAuthButton
|
||||||
|
|
||||||
|
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
|
enabled: visible
|
||||||
|
onClicked: root.startAuthSession(false)
|
||||||
|
}
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: virtualKeyboardButton
|
id: virtualKeyboardButton
|
||||||
|
|
||||||
@@ -545,7 +1103,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboard_controller.isKeyboardActive) {
|
if (keyboard_controller.isKeyboardActive) {
|
||||||
@@ -564,19 +1122,14 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: true
|
enabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (GreeterState.username) {
|
root.startAuthSession(true);
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (inputField.text.trim()) {
|
if (inputField.text.trim()) {
|
||||||
GreeterState.username = inputField.text.trim();
|
root.submitUsername(inputField.text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,20 +1154,16 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 38
|
||||||
Layout.topMargin: -Theme.spacingS
|
Layout.topMargin: -Theme.spacingS
|
||||||
Layout.bottomMargin: -Theme.spacingS
|
Layout.bottomMargin: -Theme.spacingS
|
||||||
text: {
|
text: root.authFeedbackMessage
|
||||||
if (GreeterState.pamState === "error")
|
|
||||||
return "Authentication error - try again";
|
|
||||||
if (GreeterState.pamState === "fail")
|
|
||||||
return "Incorrect password";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: GreeterState.pamState !== "" ? 1 : 0
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
opacity: root.authFeedbackMessage !== "" ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -667,6 +1216,7 @@ Item {
|
|||||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||||
onClicked: {
|
onClicked: {
|
||||||
GreeterState.reset();
|
GreeterState.reset();
|
||||||
|
root.externalAuthAutoStartedForUser = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
PortalService.profileImage = "";
|
PortalService.profileImage = "";
|
||||||
}
|
}
|
||||||
@@ -1029,9 +1579,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (!GreetdMemory.memoryReady)
|
if (!GreetdMemory.memoryReady)
|
||||||
return;
|
return;
|
||||||
|
if (!GreetdSettings.settingsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
const savedSession = GreetdMemory.lastSessionId;
|
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
|
||||||
if (savedSession) {
|
if (savedSession && GreetdSettings.rememberLastSession) {
|
||||||
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
||||||
if (GreeterState.sessionPaths[i] === savedSession) {
|
if (GreeterState.sessionPaths[i] === savedSession) {
|
||||||
GreeterState.currentSessionIndex = i;
|
GreeterState.currentSessionIndex = i;
|
||||||
@@ -1164,44 +1716,151 @@ Item {
|
|||||||
|
|
||||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||||
if (responseRequired) {
|
if (responseRequired) {
|
||||||
Greetd.respond(GreeterState.passwordBuffer);
|
cancelingExternalAuthForPassword = false;
|
||||||
GreeterState.passwordBuffer = "";
|
passwordSessionTransitionRetryCount = 0;
|
||||||
inputField.text = "";
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = true;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (!passwordSubmitRequested && hasPasswordBuffer)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
if (passwordSubmitRequested && !root.submitBufferedPassword())
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
if (passwordSubmitRequested || hasPasswordBuffer) {
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
} else {
|
||||||
|
authTimeout.stop();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!error)
|
pendingPasswordResponse = false;
|
||||||
Greetd.respond("");
|
const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
|
||||||
|
if (!passwordSubmitRequested)
|
||||||
|
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
|
||||||
|
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
|
||||||
|
authTimeout.interval = externalAuthTimeoutMs;
|
||||||
|
else
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.respond("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStateChanged() {
|
||||||
|
if (Greetd.state === GreetdState.Inactive) {
|
||||||
|
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
if (resumePasswordSubmit) {
|
||||||
|
Qt.callLater(function() {
|
||||||
|
root.startAuthSession(true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadyToLaunch() {
|
function onReadyToLaunch() {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
||||||
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
||||||
if (!sessionCmd) {
|
if (!sessionCmd) {
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterState.unlocking = true;
|
GreeterState.unlocking = true;
|
||||||
launchTimeout.restart();
|
launchTimeout.restart();
|
||||||
GreetdMemory.setLastSessionId(sessionPath);
|
if (GreetdSettings.rememberLastSession) {
|
||||||
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
GreetdMemory.setLastSessionId(sessionPath);
|
||||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
} else if (GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
if (GreetdSettings.rememberLastUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
||||||
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
pendingLaunchCommand = sessionCmd;
|
||||||
|
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||||
|
memoryFlushTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "fail";
|
if (isLikelyLockoutMessage(message)) {
|
||||||
|
GreeterState.pamState = "max";
|
||||||
|
} else {
|
||||||
|
GreeterState.pamState = "fail";
|
||||||
|
passwordFailureCount = passwordFailureCount + 1;
|
||||||
|
}
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: memoryFlushTimer
|
||||||
|
interval: memoryFlushDelayMs
|
||||||
|
onTriggered: {
|
||||||
|
if (!pendingLaunchCommand)
|
||||||
|
return;
|
||||||
|
const sessionCommand = pendingLaunchCommand;
|
||||||
|
const launchEnv = pendingLaunchEnv;
|
||||||
|
pendingLaunchCommand = "";
|
||||||
|
pendingLaunchEnv = [];
|
||||||
|
Greetd.launch(sessionCommand.split(" "), launchEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: authTimeout
|
||||||
|
interval: defaultAuthTimeoutMs
|
||||||
|
onTriggered: {
|
||||||
|
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
@@ -1215,8 +1874,11 @@ Item {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!GreeterState.unlocking)
|
if (!GreeterState.unlocking)
|
||||||
return;
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
resetPasswordSessionTransition(true);
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
Greetd.cancelSession();
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
@@ -1225,7 +1887,7 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: placeholderDelay
|
id: placeholderDelay
|
||||||
interval: 4000
|
interval: 4000
|
||||||
onTriggered: GreeterState.pamState = ""
|
onTriggered: clearAuthFeedback()
|
||||||
}
|
}
|
||||||
|
|
||||||
LockPowerMenu {
|
LockPowerMenu {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
|
|||||||
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
||||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||||
- **Session Memory**: Remembers last selected session and user
|
- **Session Memory**: Remembers last selected session and user
|
||||||
|
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
|
|||||||
dms-greeter --command sway
|
dms-greeter --command sway
|
||||||
dms-greeter --command mangowc
|
dms-greeter --command mangowc
|
||||||
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
||||||
|
dms-greeter --command niri --remember-last-user false --remember-last-session false
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure greetd to use it in `/etc/greetd/config.toml`:
|
Configure greetd to use it in `/etc/greetd/config.toml`:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ COMPOSITOR=""
|
|||||||
COMPOSITOR_CONFIG=""
|
COMPOSITOR_CONFIG=""
|
||||||
DMS_PATH="dms-greeter"
|
DMS_PATH="dms-greeter"
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
CACHE_DIR="/var/cache/dms-greeter"
|
||||||
|
REMEMBER_LAST_SESSION=""
|
||||||
|
REMEMBER_LAST_USER=""
|
||||||
|
DEBUG_MODE=0
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -22,6 +25,15 @@ Options:
|
|||||||
(default: dms-greeter)
|
(default: dms-greeter)
|
||||||
--cache-dir PATH Cache directory for greeter data
|
--cache-dir PATH Cache directory for greeter data
|
||||||
(default: /var/cache/dms-greeter)
|
(default: /var/cache/dms-greeter)
|
||||||
|
--remember-last-session BOOL
|
||||||
|
Persist selected session to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--remember-last-user BOOL
|
||||||
|
Persist last successful username to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--no-save-session Alias for --remember-last-session false
|
||||||
|
--no-save-username Alias for --remember-last-user false
|
||||||
|
--debug Enable verbose startup logging to stderr
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -30,6 +42,7 @@ Examples:
|
|||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
||||||
|
dms-greeter --command niri --no-save-session --no-save-username
|
||||||
dms-greeter --command mango
|
dms-greeter --command mango
|
||||||
dms-greeter --command labwc
|
dms-greeter --command labwc
|
||||||
EOF
|
EOF
|
||||||
@@ -43,6 +56,40 @@ require_command() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalize_bool_flag() {
|
||||||
|
local flag_name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local normalized="${value,,}"
|
||||||
|
|
||||||
|
case "$normalized" in
|
||||||
|
1|true|yes|on)
|
||||||
|
echo "1"
|
||||||
|
;;
|
||||||
|
0|false|no|off)
|
||||||
|
echo "0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_compositor() {
|
||||||
|
local log_tag="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemd-cat >/dev/null 2>&1; then
|
||||||
|
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--command)
|
--command)
|
||||||
@@ -61,6 +108,26 @@ while [[ $# -gt 0 ]]; do
|
|||||||
CACHE_DIR="$2"
|
CACHE_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--remember-last-session)
|
||||||
|
REMEMBER_LAST_SESSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--remember-last-user)
|
||||||
|
REMEMBER_LAST_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-save-session)
|
||||||
|
REMEMBER_LAST_SESSION="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-save-username)
|
||||||
|
REMEMBER_LAST_USER="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
DEBUG_MODE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -111,9 +178,62 @@ export QT_QPA_PLATFORM=wayland
|
|||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
||||||
export EGL_PLATFORM=gbm
|
export EGL_PLATFORM=gbm
|
||||||
export DMS_RUN_GREETER=1
|
export DMS_RUN_GREETER=1
|
||||||
|
|
||||||
|
if [[ ! -d "$CACHE_DIR" ]]; then
|
||||||
|
echo "Error: cache directory '$CACHE_DIR' does not exist." >&2
|
||||||
|
echo " Run 'dms greeter sync' to initialize it, or pass --cache-dir to an existing directory." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_SESSION
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
||||||
|
DMS_SAVE_SESSION=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_SESSION=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_SESSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_USER
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
||||||
|
DMS_SAVE_USERNAME=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_USERNAME=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_USERNAME
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOME="$CACHE_DIR"
|
||||||
|
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
||||||
|
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
||||||
|
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
||||||
|
|
||||||
|
# Propagate correct XDG dirs into the systemd user session so socket-activated
|
||||||
|
# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd.
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl --user set-environment \
|
||||||
|
HOME="$CACHE_DIR" \
|
||||||
|
XDG_STATE_HOME="$CACHE_DIR/.local/state" \
|
||||||
|
XDG_DATA_HOME="$CACHE_DIR/.local/share" \
|
||||||
|
XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true
|
||||||
|
if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then
|
||||||
|
systemctl --user restart wireplumber.service 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep greeter VT clean by default; callers can override via env or --debug.
|
||||||
|
if [[ -z "${RUST_LOG:-}" ]]; then
|
||||||
|
export RUST_LOG=warn
|
||||||
|
fi
|
||||||
|
if [[ -z "${NIRI_LOG:-}" ]]; then
|
||||||
|
export NIRI_LOG=warn
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
if command -v qs >/dev/null 2>&1; then
|
||||||
QS_BIN="qs"
|
QS_BIN="qs"
|
||||||
@@ -130,7 +250,9 @@ if [[ "$DMS_PATH" == /* ]]; then
|
|||||||
else
|
else
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
||||||
|
fi
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
||||||
else
|
else
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
||||||
@@ -192,7 +314,7 @@ NIRI_EOF
|
|||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
||||||
NIRI_EOF
|
NIRI_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
exec niri -c "$COMPOSITOR_CONFIG"
|
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
hyprland)
|
hyprland)
|
||||||
@@ -222,9 +344,9 @@ HYPRLAND_EOF
|
|||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
if command -v start-hyprland >/dev/null 2>&1; then
|
||||||
exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
||||||
else
|
else
|
||||||
exec Hyprland -c "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -245,7 +367,7 @@ exec "$QS_CMD; swaymsg exit"
|
|||||||
SWAY_EOF
|
SWAY_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
scroll)
|
scroll)
|
||||||
@@ -265,7 +387,7 @@ exec "$QS_CMD; scrollmsg exit"
|
|||||||
SCROLL_EOF
|
SCROLL_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec scroll -c "$COMPOSITOR_CONFIG"
|
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
miracle|miracle-wm)
|
miracle|miracle-wm)
|
||||||
@@ -285,24 +407,24 @@ exec "$QS_CMD; miraclemsg exit"
|
|||||||
MIRACLE_EOF
|
MIRACLE_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec miracle-wm -c "$COMPOSITOR_CONFIG"
|
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
labwc)
|
labwc)
|
||||||
require_command "labwc"
|
require_command "labwc"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||||
else
|
else
|
||||||
exec labwc --session "$QS_CMD"
|
exec_compositor "labwc" labwc --session "$QS_CMD"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
mango|mangowc)
|
mango|mangowc)
|
||||||
require_command "mango"
|
require_command "mango"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
||||||
else
|
else
|
||||||
exec mango -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Scope {
|
|||||||
property bool shouldLock: false
|
property bool shouldLock: false
|
||||||
|
|
||||||
onShouldLockChanged: {
|
onShouldLockChanged: {
|
||||||
|
IdleService.isShellLocked = shouldLock;
|
||||||
if (shouldLock && lockPowerOffArmed) {
|
if (shouldLock && lockPowerOffArmed) {
|
||||||
lockStateCheck.restart();
|
lockStateCheck.restart();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -745,8 +745,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active) {
|
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
|
||||||
console.log("Enter pressed, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -755,6 +754,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.unlocking) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
@@ -998,8 +1002,7 @@ Item {
|
|||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode) {
|
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||||
console.log("Enter button clicked, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1602,6 +1605,7 @@ Item {
|
|||||||
onStateChanged: {
|
onStateChanged: {
|
||||||
root.pamState = state;
|
root.pamState = state;
|
||||||
if (state !== "") {
|
if (state !== "") {
|
||||||
|
root.unlocking = false;
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
@@ -1609,6 +1613,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: pam
|
||||||
|
|
||||||
|
function onUnlockInProgressChanged() {
|
||||||
|
if (!pam.unlockInProgress && root.unlocking)
|
||||||
|
root.unlocking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: pam
|
target: pam
|
||||||
property: "buffer"
|
property: "buffer"
|
||||||
|
|||||||
+205
-12
@@ -22,6 +22,64 @@ Scope {
|
|||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
|
function resetAuthFlows(): void {
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recoverFromAuthStall(newState: string): void {
|
||||||
|
resetAuthFlows();
|
||||||
|
state = newState;
|
||||||
|
flashMsg();
|
||||||
|
stateReset.restart();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeUnlock(): void {
|
||||||
|
if (!unlockInProgress) {
|
||||||
|
unlockInProgress = true;
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockRequestTimeout.restart();
|
||||||
|
unlockRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proceedAfterPrimaryAuth(): void {
|
||||||
|
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
|
||||||
|
u2f.startForSecondFactor();
|
||||||
|
} else {
|
||||||
|
completeUnlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelU2fPending(): void {
|
||||||
|
if (!u2fPending)
|
||||||
|
return;
|
||||||
|
u2f.abort();
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
fprint.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dankshellConfigWatcher
|
id: dankshellConfigWatcher
|
||||||
|
|
||||||
@@ -66,6 +124,13 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
root.unlockInProgress = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2f.abort();
|
||||||
|
|
||||||
if (res === PamResult.Error)
|
if (res === PamResult.Error)
|
||||||
root.state = "error";
|
root.state = "error";
|
||||||
else if (res === PamResult.MaxTries)
|
else if (res === PamResult.MaxTries)
|
||||||
@@ -78,10 +143,22 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: passwd
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
if (passwd.active) {
|
||||||
|
passwdActiveTimeout.restart();
|
||||||
|
} else {
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PamContext {
|
PamContext {
|
||||||
id: fprint
|
id: fprint
|
||||||
|
|
||||||
property bool available
|
property bool available: SettingsData.lockFingerprintReady
|
||||||
property int tries
|
property int tries
|
||||||
property int errorTries
|
property int errorTries
|
||||||
|
|
||||||
@@ -135,13 +212,71 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
PamContext {
|
||||||
id: availProc
|
id: u2f
|
||||||
|
|
||||||
command: ["sh", "-c", "fprintd-list $USER"]
|
property bool available: SettingsData.lockU2fReady
|
||||||
onExited: code => {
|
|
||||||
fprint.available = code === 0;
|
function checkAvail(): void {
|
||||||
fprint.checkAvail();
|
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
|
||||||
|
abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsData.u2fMode === "or") {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startForSecondFactor(): void {
|
||||||
|
if (!available || !SettingsData.enableU2f) {
|
||||||
|
root.completeUnlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
abort();
|
||||||
|
root.u2fPending = true;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.restart();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
|
||||||
|
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
|
onMessageChanged: {
|
||||||
|
if (message !== "")
|
||||||
|
root.u2fState = "waiting";
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompleted: res => {
|
||||||
|
if (!available || root.unlockInProgress)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (res === PamResult.Success) {
|
||||||
|
root.completeUnlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res === PamResult.Error || res === PamResult.MaxTries || res === PamResult.Failed) {
|
||||||
|
abort();
|
||||||
|
|
||||||
|
if (root.u2fPending) {
|
||||||
|
if (root.u2fState === "waiting") {
|
||||||
|
// AND mode: device was found but auth failed → back to password
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
fprint.checkAvail();
|
||||||
|
} else {
|
||||||
|
// AND mode: no device found → keep pending, show "Insert...", retry
|
||||||
|
root.u2fState = "insert";
|
||||||
|
u2fErrorRetry.restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// OR mode: prompt to insert key, silently retry
|
||||||
|
root.u2fState = "insert";
|
||||||
|
u2fErrorRetry.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +287,40 @@ Scope {
|
|||||||
onTriggered: fprint.start()
|
onTriggered: fprint.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fErrorRetry
|
||||||
|
|
||||||
|
interval: 800
|
||||||
|
onTriggered: u2f.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fPendingTimeout
|
||||||
|
|
||||||
|
interval: 30000
|
||||||
|
onTriggered: root.cancelU2fPending()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: passwdActiveTimeout
|
||||||
|
|
||||||
|
interval: 15000
|
||||||
|
onTriggered: {
|
||||||
|
if (passwd.active)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unlockRequestTimeout
|
||||||
|
|
||||||
|
interval: 8000
|
||||||
|
onTriggered: {
|
||||||
|
if (root.unlockInProgress)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -174,15 +343,15 @@ Scope {
|
|||||||
|
|
||||||
onLockSecuredChanged: {
|
onLockSecuredChanged: {
|
||||||
if (lockSecured) {
|
if (lockSecured) {
|
||||||
availProc.running = true;
|
SettingsData.refreshAuthAvailability();
|
||||||
root.state = "";
|
root.state = "";
|
||||||
root.fprintState = "";
|
root.fprintState = "";
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.resetAuthFlows();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
root.resetAuthFlows();
|
||||||
passwd.abort();
|
|
||||||
root.unlockInProgress = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,5 +361,29 @@ Scope {
|
|||||||
function onEnableFprintChanged(): void {
|
function onEnableFprintChanged(): void {
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onLockFingerprintReadyChanged(): void {
|
||||||
|
fprint.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnableU2fChanged(): void {
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLockU2fReadyChanged(): void {
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onU2fModeChanged(): void {
|
||||||
|
if (root.lockSecured) {
|
||||||
|
u2f.abort();
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Item {
|
|||||||
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
|
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
|
||||||
property bool showSettingsMenu: false
|
property bool showSettingsMenu: false
|
||||||
property string pendingSaveContent: ""
|
property string pendingSaveContent: ""
|
||||||
|
property var slideout: null
|
||||||
|
|
||||||
signal hideRequested
|
signal hideRequested
|
||||||
signal previewRequested(string content)
|
signal previewRequested(string content)
|
||||||
@@ -29,6 +30,14 @@ Item {
|
|||||||
service: NotepadStorageService
|
service: NotepadStorageService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: slideout
|
||||||
|
enabled: slideout !== null
|
||||||
|
function onAboutToHide() {
|
||||||
|
textEditor.autoSaveToSession()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasUnsavedChanges() {
|
function hasUnsavedChanges() {
|
||||||
return textEditor.hasUnsavedChanges();
|
return textEditor.hasUnsavedChanges();
|
||||||
}
|
}
|
||||||
@@ -204,7 +213,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onEscapePressed: {
|
onEscapePressed: {
|
||||||
root.hideRequested();
|
textEditor.autoSaveToSession()
|
||||||
|
root.hideRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
onSettingsRequested: {
|
onSettingsRequested: {
|
||||||
|
|||||||
@@ -555,7 +555,9 @@ Column {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
function onTabsChanged() {
|
function onTabsChanged() {
|
||||||
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {/* Lines 444-445 omitted */}
|
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
|
||||||
|
loadCurrentTabContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,13 +133,20 @@ DankPopout {
|
|||||||
|
|
||||||
property var externalKeyboardController: null
|
property var externalKeyboardController: null
|
||||||
property real cachedHeaderHeight: 32
|
property real cachedHeaderHeight: 32
|
||||||
|
readonly property real settingsMaxHeight: {
|
||||||
|
const screenH = root.screen ? root.screen.height : 1080;
|
||||||
|
const maxPopupH = screenH * 0.8;
|
||||||
|
const overhead = cachedHeaderHeight + Theme.spacingL * 2 + Theme.spacingM * 2;
|
||||||
|
return Math.max(200, maxPopupH - overhead - 150);
|
||||||
|
}
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
let baseHeight = Theme.spacingL * 2;
|
let baseHeight = Theme.spacingL * 2;
|
||||||
baseHeight += cachedHeaderHeight;
|
baseHeight += cachedHeaderHeight;
|
||||||
baseHeight += Theme.spacingM * 2;
|
baseHeight += Theme.spacingM * 2;
|
||||||
|
|
||||||
const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0;
|
const settingsHeight = notificationSettings.expanded ? Math.min(notificationSettings.naturalContentHeight, notificationContent.settingsMaxHeight) : 0;
|
||||||
let listHeight = notificationHeader.currentTab === 0 ? notificationList.stableContentHeight : Math.max(200, NotificationService.historyList.length * 80);
|
const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight;
|
||||||
|
let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80);
|
||||||
if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
|
if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
|
||||||
listHeight = 200;
|
listHeight = 200;
|
||||||
}
|
}
|
||||||
@@ -231,6 +238,7 @@ DankPopout {
|
|||||||
NotificationSettings {
|
NotificationSettings {
|
||||||
id: notificationSettings
|
id: notificationSettings
|
||||||
expanded: notificationHeader.showSettings
|
expanded: notificationHeader.showSettings
|
||||||
|
maxAllowedHeight: notificationContent.settingsMaxHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardNavigatedNotificationList {
|
KeyboardNavigatedNotificationList {
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ Rectangle {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2
|
property real maxAllowedHeight: 0
|
||||||
|
readonly property real naturalContentHeight: contentColumn.height + Theme.spacingL * 2
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: expanded ? contentHeight : 0
|
height: expanded ? (maxAllowedHeight > 0 ? Math.min(naturalContentHeight, maxAllowedHeight) : naturalContentHeight) : 0
|
||||||
visible: expanded
|
visible: expanded
|
||||||
clip: true
|
clip: true
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
@@ -105,13 +106,22 @@ Rectangle {
|
|||||||
return Math.round(value / 60000) + " minutes";
|
return Math.round(value / 60000) + " minutes";
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Flickable {
|
||||||
id: contentColumn
|
id: settingsFlickable
|
||||||
anchors.top: parent.top
|
anchors.fill: parent
|
||||||
anchors.left: parent.left
|
contentHeight: contentColumn.height + Theme.spacingL * 2
|
||||||
anchors.right: parent.right
|
clip: true
|
||||||
anchors.margins: Theme.spacingL
|
flickableDirection: Flickable.VerticalFlick
|
||||||
spacing: Theme.spacingM
|
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||||
|
interactive: root.naturalContentHeight > root.height
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: contentColumn
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Notification Settings")
|
text: I18n.tr("Notification Settings")
|
||||||
@@ -421,4 +431,5 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,51 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
|
||||||
|
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
|
||||||
|
|
||||||
|
function lockFingerprintDescription() {
|
||||||
|
switch (SettingsData.lockFingerprintReason) {
|
||||||
|
case "ready":
|
||||||
|
return I18n.tr("Use fingerprint authentication for the lock screen.");
|
||||||
|
case "missing_enrollment":
|
||||||
|
if (SettingsData.enableFprint)
|
||||||
|
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.");
|
||||||
|
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
|
||||||
|
case "missing_reader":
|
||||||
|
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
|
||||||
|
case "missing_pam_support":
|
||||||
|
return I18n.tr("Not available — install fprintd and pam_fprintd.");
|
||||||
|
default:
|
||||||
|
return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockU2fDescription() {
|
||||||
|
switch (SettingsData.lockU2fReason) {
|
||||||
|
case "ready":
|
||||||
|
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
|
||||||
|
case "missing_key_registration":
|
||||||
|
if (SettingsData.enableU2f)
|
||||||
|
return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config.");
|
||||||
|
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":
|
||||||
|
return I18n.tr("Not available — install or configure pam_u2f.");
|
||||||
|
default:
|
||||||
|
return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthDetection() {
|
||||||
|
SettingsData.refreshAuthAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: refreshAuthDetection()
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
refreshAuthDetection();
|
||||||
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -161,11 +206,39 @@ Item {
|
|||||||
settingKey: "enableFprint"
|
settingKey: "enableFprint"
|
||||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||||
text: I18n.tr("Enable fingerprint authentication")
|
text: I18n.tr("Enable fingerprint authentication")
|
||||||
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
|
description: root.lockFingerprintDescription()
|
||||||
|
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||||
checked: SettingsData.enableFprint
|
checked: SettingsData.enableFprint
|
||||||
visible: SettingsData.fprintdAvailable
|
enabled: root.lockFprintToggleAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "enableU2f"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||||
|
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
||||||
|
description: root.lockU2fDescription()
|
||||||
|
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.enableU2f
|
||||||
|
enabled: root.lockU2fToggleAvailable
|
||||||
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "u2fMode"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||||
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||||
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||||
|
visible: SettingsData.enableU2f
|
||||||
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||||
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
|
||||||
|
SettingsData.set("u2fMode", "and");
|
||||||
|
else
|
||||||
|
SettingsData.set("u2fMode", "or");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
@@ -84,17 +84,19 @@ Variants {
|
|||||||
readonly property bool transitioning: transitionAnimation.running
|
readonly property bool transitioning: transitionAnimation.running
|
||||||
property bool effectActive: false
|
property bool effectActive: false
|
||||||
property bool _renderSettling: true
|
property bool _renderSettling: true
|
||||||
|
property bool _overviewBlurSettling: false
|
||||||
property bool useNextForEffect: false
|
property bool useNextForEffect: false
|
||||||
property string pendingWallpaper: ""
|
property string pendingWallpaper: ""
|
||||||
property string _deferredSource: ""
|
property string _deferredSource: ""
|
||||||
|
readonly property bool overviewBlurActive: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: currentWallpaper
|
target: currentWallpaper
|
||||||
function onStatusChanged() {
|
function onStatusChanged() {
|
||||||
if (currentWallpaper.status === Image.Ready) {
|
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||||
root._renderSettling = true;
|
return;
|
||||||
renderSettleTimer.restart();
|
root._renderSettling = true;
|
||||||
}
|
renderSettleTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +122,22 @@ Variants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NiriService
|
||||||
|
function onInOverviewChanged() {
|
||||||
|
root._overviewBlurSettling = true;
|
||||||
|
overviewBlurSettleTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SettingsData
|
||||||
|
function onBlurWallpaperOnOverviewChanged() {
|
||||||
|
root._overviewBlurSettling = true;
|
||||||
|
overviewBlurSettleTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: CompositorService
|
target: CompositorService
|
||||||
function onRandrDataReady() {
|
function onRandrDataReady() {
|
||||||
@@ -133,12 +151,28 @@ Variants {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: IdleService
|
||||||
|
function onIsShellLockedChanged() {
|
||||||
|
if (!IdleService.isShellLocked) {
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: renderSettleTimer
|
id: renderSettleTimer
|
||||||
interval: 1000
|
interval: 1000
|
||||||
onTriggered: root._renderSettling = false
|
onTriggered: root._renderSettling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: overviewBlurSettleTimer
|
||||||
|
interval: 150
|
||||||
|
onTriggered: root._overviewBlurSettling = false
|
||||||
|
}
|
||||||
|
|
||||||
function getFillMode(modeName) {
|
function getFillMode(modeName) {
|
||||||
switch (modeName) {
|
switch (modeName) {
|
||||||
case "Stretch":
|
case "Stretch":
|
||||||
@@ -164,7 +198,7 @@ Variants {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (typeof wallpaperWindow.updatesEnabled !== "undefined")
|
if (typeof wallpaperWindow.updatesEnabled !== "undefined")
|
||||||
wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
wallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || root.overviewBlurActive || root._overviewBlurSettling || root.pendingWallpaper !== "" || root._deferredSource !== "" || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
root._renderSettling = false;
|
root._renderSettling = false;
|
||||||
@@ -296,6 +330,7 @@ Variants {
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
layer.enabled: false
|
layer.enabled: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -309,6 +344,7 @@ Variants {
|
|||||||
opacity: 0
|
opacity: 0
|
||||||
layer.enabled: false
|
layer.enabled: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -567,6 +603,8 @@ Variants {
|
|||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
currentWallpaper.layer.enabled = false;
|
currentWallpaper.layer.enabled = false;
|
||||||
nextWallpaper.layer.enabled = false;
|
nextWallpaper.layer.enabled = false;
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
|
|
||||||
if (!root.pendingWallpaper)
|
if (!root.pendingWallpaper)
|
||||||
@@ -578,8 +616,9 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
id: overviewBlurLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
active: CompositorService.isNiri && SettingsData.blurWallpaperOnOverview && NiriService.inOverview && currentWallpaper.source !== ""
|
active: root.overviewBlurActive
|
||||||
|
|
||||||
sourceComponent: MultiEffect {
|
sourceComponent: MultiEffect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ Singleton {
|
|||||||
property var suspendMonitor: null
|
property var suspendMonitor: null
|
||||||
property var lockComponent: null
|
property var lockComponent: null
|
||||||
property bool monitorsOff: false
|
property bool monitorsOff: false
|
||||||
|
property bool isShellLocked: false
|
||||||
|
|
||||||
function wake() {
|
function wake() {
|
||||||
requestMonitorOn();
|
requestMonitorOn();
|
||||||
|
|||||||
@@ -194,10 +194,11 @@ Singleton {
|
|||||||
var timer = monitorTimers[screenName];
|
var timer = monitorTimers[screenName];
|
||||||
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
|
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
|
||||||
var newTimers = Object.assign({}, monitorTimers);
|
var newTimers = Object.assign({}, monitorTimers);
|
||||||
newTimers[screenName] = monitorTimerComponent.createObject(root);
|
var newTimer = monitorTimerComponent.createObject(root);
|
||||||
newTimers[screenName].targetScreen = screenName;
|
newTimer.targetScreen = screenName;
|
||||||
|
newTimers[screenName] = newTimer;
|
||||||
monitorTimers = newTimers;
|
monitorTimers = newTimers;
|
||||||
timer = monitorTimers[screenName];
|
timer = newTimer;
|
||||||
}
|
}
|
||||||
if (timer) {
|
if (timer) {
|
||||||
timer.interval = settings.interval * 1000;
|
timer.interval = settings.interval * 1000;
|
||||||
@@ -258,9 +259,10 @@ Singleton {
|
|||||||
var process = monitorProcesses[screenName];
|
var process = monitorProcesses[screenName];
|
||||||
if (!process) {
|
if (!process) {
|
||||||
var newProcesses = Object.assign({}, monitorProcesses);
|
var newProcesses = Object.assign({}, monitorProcesses);
|
||||||
newProcesses[screenName] = monitorProcessComponent.createObject(root);
|
var newProcess = monitorProcessComponent.createObject(root);
|
||||||
|
newProcesses[screenName] = newProcess;
|
||||||
monitorProcesses = newProcesses;
|
monitorProcesses = newProcesses;
|
||||||
process = monitorProcesses[screenName];
|
process = newProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process) {
|
if (process) {
|
||||||
@@ -290,9 +292,10 @@ Singleton {
|
|||||||
var process = monitorProcesses[screenName];
|
var process = monitorProcesses[screenName];
|
||||||
if (!process) {
|
if (!process) {
|
||||||
var newProcesses = Object.assign({}, monitorProcesses);
|
var newProcesses = Object.assign({}, monitorProcesses);
|
||||||
newProcesses[screenName] = monitorProcessComponent.createObject(root);
|
var newProcess = monitorProcessComponent.createObject(root);
|
||||||
|
newProcesses[screenName] = newProcess;
|
||||||
monitorProcesses = newProcesses;
|
monitorProcesses = newProcesses;
|
||||||
process = monitorProcesses[screenName];
|
process = newProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process) {
|
if (process) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ PanelWindow {
|
|||||||
property string title: ""
|
property string title: ""
|
||||||
property alias container: contentContainer
|
property alias container: contentContainer
|
||||||
property real customTransparency: -1
|
property real customTransparency: -1
|
||||||
|
signal aboutToHide
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
visible = true
|
visible = true
|
||||||
@@ -32,6 +33,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
aboutToHide()
|
||||||
isVisible = false
|
isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#%PAM-1.0
|
#%PAM-1.0
|
||||||
|
|
||||||
auth required pam_fprintd.so max-tries=1
|
auth required pam_fprintd.so max-tries=1 timeout=5
|
||||||
|
|||||||
@@ -2484,23 +2484,6 @@
|
|||||||
"theme"
|
"theme"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"section": "matugenTemplateZed",
|
|
||||||
"label": "Zed",
|
|
||||||
"tabIndex": 10,
|
|
||||||
"category": "Theme & Colors",
|
|
||||||
"keywords": [
|
|
||||||
"appearance",
|
|
||||||
"colors",
|
|
||||||
"look",
|
|
||||||
"matugen",
|
|
||||||
"scheme",
|
|
||||||
"style",
|
|
||||||
"template",
|
|
||||||
"theme",
|
|
||||||
"zed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"section": "matugenTemplateFirefox",
|
"section": "matugenTemplateFirefox",
|
||||||
"label": "Firefox",
|
"label": "Firefox",
|
||||||
@@ -3535,6 +3518,23 @@
|
|||||||
],
|
],
|
||||||
"description": "Rounded corners for windows (decoration.rounding)"
|
"description": "Rounded corners for windows (decoration.rounding)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "matugenTemplateZed",
|
||||||
|
"label": "Zed",
|
||||||
|
"tabIndex": 10,
|
||||||
|
"category": "Theme & Colors",
|
||||||
|
"keywords": [
|
||||||
|
"appearance",
|
||||||
|
"colors",
|
||||||
|
"look",
|
||||||
|
"matugen",
|
||||||
|
"scheme",
|
||||||
|
"style",
|
||||||
|
"template",
|
||||||
|
"theme",
|
||||||
|
"zed"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "matugenTemplateDgop",
|
"section": "matugenTemplateDgop",
|
||||||
"label": "dgop",
|
"label": "dgop",
|
||||||
@@ -3775,18 +3775,14 @@
|
|||||||
"authentication",
|
"authentication",
|
||||||
"biometric",
|
"biometric",
|
||||||
"enable",
|
"enable",
|
||||||
"enrolled",
|
|
||||||
"fingerprint",
|
"fingerprint",
|
||||||
"fprint",
|
"fprint",
|
||||||
"lock",
|
"lock",
|
||||||
"lockscreen",
|
|
||||||
"login",
|
"login",
|
||||||
"password",
|
"password",
|
||||||
"reader",
|
|
||||||
"screen",
|
"screen",
|
||||||
"security"
|
"security"
|
||||||
],
|
]
|
||||||
"description": "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": "loginctlLockIntegration",
|
"section": "loginctlLockIntegration",
|
||||||
@@ -3811,6 +3807,26 @@
|
|||||||
],
|
],
|
||||||
"description": "Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen"
|
"description": "Bind lock screen to dbus signals from loginctl. Disable if using an external lock screen"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "enableU2f",
|
||||||
|
"label": "Enable security key authentication",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"authentication",
|
||||||
|
"enable",
|
||||||
|
"fido",
|
||||||
|
"hardware",
|
||||||
|
"key",
|
||||||
|
"lock",
|
||||||
|
"login",
|
||||||
|
"password",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"u2f",
|
||||||
|
"yubikey"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "lockDisplay",
|
"section": "lockDisplay",
|
||||||
"label": "Lock Screen Display",
|
"label": "Lock Screen Display",
|
||||||
@@ -3982,6 +3998,25 @@
|
|||||||
],
|
],
|
||||||
"description": "Turn off all displays immediately when the lock screen activates"
|
"description": "Turn off all displays immediately when the lock screen activates"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "u2fMode",
|
||||||
|
"label": "Security key mode",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"factor",
|
||||||
|
"key",
|
||||||
|
"lock",
|
||||||
|
"login",
|
||||||
|
"mode",
|
||||||
|
"password",
|
||||||
|
"screen",
|
||||||
|
"second",
|
||||||
|
"security",
|
||||||
|
"u2f",
|
||||||
|
"yubikey"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "lockScreenShowMediaPlayer",
|
"section": "lockScreenShowMediaPlayer",
|
||||||
"label": "Show Media Player",
|
"label": "Show Media Player",
|
||||||
|
|||||||
Reference in New Issue
Block a user