mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 02:52:07 -04:00
Compare commits
17 Commits
v1.4.4.1
...
088ed806ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
088ed806ae | ||
|
|
07d2c94676 | ||
|
|
0bc1b7a3c2 | ||
|
|
c5987b28c0 | ||
|
|
18901c7cde | ||
|
|
519a8357a1 | ||
|
|
799773c62b | ||
|
|
247a674c79 | ||
|
|
72b598057c | ||
|
|
8180e30e8e | ||
|
|
dd9851b4f0 | ||
|
|
056576fbe7 | ||
|
|
d28a5bdf7f | ||
|
|
f62ea119f7 | ||
|
|
a1f9b98727 | ||
|
|
b9c8914d46 | ||
|
|
8697840d46 |
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var ipcCmd = &cobra.Command{
|
||||
Use: "ipc [target] [function] [args...]",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
PreRunE: findConfig,
|
||||
Use: "ipc [target] [function] [args...]",
|
||||
Short: "Send IPC commands to running DMS shell",
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
_ = findConfig(cmd, args)
|
||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
|
||||
@@ -109,16 +109,41 @@ func updateArchLinux() error {
|
||||
}
|
||||
|
||||
var packageName string
|
||||
if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
var isAUR bool
|
||||
if isArchPackageInstalled("dms-shell") {
|
||||
packageName = "dms-shell"
|
||||
} else if isArchPackageInstalled("dms-shell-git") {
|
||||
packageName = "dms-shell-git"
|
||||
isAUR = true
|
||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
isAUR = true
|
||||
} else {
|
||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||
fmt.Println("Info: No dms-shell package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
return updateOtherDistros()
|
||||
}
|
||||
|
||||
if !isAUR {
|
||||
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||
if !confirmUpdate() {
|
||||
return errdefs.ErrUpdateCancelled
|
||||
}
|
||||
|
||||
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
||||
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("dms successfully updated")
|
||||
return nil
|
||||
}
|
||||
|
||||
var helper string
|
||||
var updateCmd *exec.Cmd
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,8 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,8 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
@@ -616,6 +616,43 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFirstDMSPID() (int, bool) {
|
||||
dir := getRuntimeDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if proc.Signal(syscall.Signal(0)) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return pid, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func runShellIPCCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
printIPCHelp()
|
||||
@@ -627,10 +664,21 @@ func runShellIPCCommand(args []string) {
|
||||
}
|
||||
|
||||
cmdArgs := []string{"ipc"}
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
|
||||
switch pid, ok := getFirstDMSPID(); {
|
||||
case ok:
|
||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||
default:
|
||||
if err := findConfig(nil, nil); err != nil {
|
||||
log.Fatalf("Error finding config: %v", err)
|
||||
}
|
||||
// ! TODO - remove check when QS 0.3 is released
|
||||
if qsHasAnyDisplay() {
|
||||
cmdArgs = append(cmdArgs, "--any-display")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
cmd := exec.Command("qs", cmdArgs...)
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,100 +12,166 @@ import (
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||
|
||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||
func MaybeServeAndExit() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimeType := os.Getenv(envMime)
|
||||
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||
cachePath := os.Getenv(envCacheFile)
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case cachePath != "":
|
||||
data, err = os.ReadFile(cachePath)
|
||||
os.Remove(cachePath)
|
||||
default:
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func Copy(data []byte, mimeType string) error {
|
||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||
return copyForkCached(data, mimeType, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
total := 0
|
||||
for total < len(data) {
|
||||
n, err := writer.Write(data[total:])
|
||||
total += n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if total != len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
return serveClipboard(data, mimeType, pasteOnce)
|
||||
}
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
return copyForkCached(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
if foreground {
|
||||
buf, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source: %w", err)
|
||||
}
|
||||
return serveClipboard(buf, mimeType, pasteOnce)
|
||||
}
|
||||
return copyServeReader(data, mimeType, pasteOnce)
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = nil
|
||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||
cmd := exec.Command(os.Args[0])
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
cmd.Env = append(os.Environ(),
|
||||
envServe+"=1",
|
||||
envMime+"="+mimeType,
|
||||
)
|
||||
if pasteOnce {
|
||||
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||
}
|
||||
cmd.Env = append(cmd.Env, extra...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
func waitReady(cmd *exec.Cmd) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cachedData, err := createClipboardCacheFile()
|
||||
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||
cacheFile, err := createClipboardCacheFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
||||
return fmt.Errorf("create cache file: %w", err)
|
||||
}
|
||||
defer os.Remove(cachedData.Name())
|
||||
cachePath := cacheFile.Name()
|
||||
|
||||
if _, err := io.Copy(cachedData, data); err != nil {
|
||||
return fmt.Errorf("cache clipboard data: %w", err)
|
||||
if _, err := cacheFile.Write(data); err != nil {
|
||||
cacheFile.Close()
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("write cache file: %w", err)
|
||||
}
|
||||
if err := cachedData.Close(); err != nil {
|
||||
return fmt.Errorf("close temp cache file: %w", err)
|
||||
if err := cacheFile.Close(); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("close cache file: %w", err)
|
||||
}
|
||||
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
cachedFile, err := os.Open(cachedData.Name())
|
||||
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||
cmd.Stdin = nil
|
||||
if err := waitReady(cmd); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cmd := newForkCmd(mimeType, pasteOnce)
|
||||
|
||||
switch src := data.(type) {
|
||||
case *os.File:
|
||||
cmd.Stdin = src
|
||||
return waitReady(cmd)
|
||||
|
||||
default:
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("open temp cache file: %w", err)
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
defer cachedFile.Close()
|
||||
|
||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
||||
return fmt.Errorf("write clipboard data: %w", err)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(stdin, data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write stdin: %w", err)
|
||||
}
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
}
|
||||
}
|
||||
|
||||
func signalReady() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
return
|
||||
}
|
||||
os.Stdout.Write([]byte{1})
|
||||
}
|
||||
|
||||
func createClipboardCacheFile() (*os.File, error) {
|
||||
@@ -129,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
||||
return os.CreateTemp("", "dms-clipboard-*")
|
||||
}
|
||||
|
||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -171,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -215,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
_, _ = file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -242,13 +299,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
signalReady()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
@@ -502,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -535,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
file.Write(data)
|
||||
_, _ = file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
@@ -242,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
if a.packageInstalled("dms-shell-bin") {
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
@@ -540,7 +536,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||
var dmsShell []string
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
if pkg == "dms-shell-git" {
|
||||
dmsShell = append(dmsShell, pkg)
|
||||
} else {
|
||||
isDep := false
|
||||
@@ -621,7 +617,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
if pkg == "dms-shell-git" {
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
depsToRemove := []string{
|
||||
"depends = quickshell",
|
||||
@@ -644,15 +640,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
|
||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||
if pkg == "dms-shell-bin" {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||
}
|
||||
} else {
|
||||
{
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||
@@ -739,42 +727,9 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
CommandInfo: "sudo pacman -U built-package",
|
||||
}
|
||||
|
||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||
var files []string
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
// For DMS split packages, install base package
|
||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil {
|
||||
for _, match := range matches {
|
||||
basename := filepath.Base(match)
|
||||
// Always include base package
|
||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||
files = append(files, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also update compositor-specific packages if they're installed
|
||||
if strings.HasSuffix(pkg, "-git") {
|
||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||
files = append(files, hyprlandMatches[0])
|
||||
}
|
||||
}
|
||||
if a.packageInstalled("dms-shell-niri-git") {
|
||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||
files = append(files, niriMatches[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other packages, install all built packages
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
}
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("no package files found after building %s", pkg)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -59,7 +60,11 @@ func Send(n Notification) error {
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
if n.FilePath != "" {
|
||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
||||
imgPath := n.FilePath
|
||||
if !strings.HasPrefix(imgPath, "file://") {
|
||||
imgPath = "file://" + imgPath
|
||||
}
|
||||
hints["image_path"] = dbus.MakeVariant(imgPath)
|
||||
}
|
||||
|
||||
obj := conn.Object(notifyDest, notifyPath)
|
||||
|
||||
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
||||
m := &Manager{
|
||||
display: display,
|
||||
ctx: display.Context(),
|
||||
cmdq: make(chan cmd, 128),
|
||||
cmdq: make(chan cmd, 512),
|
||||
stopChan: make(chan struct{}),
|
||||
dirty: make(chan struct{}, 1),
|
||||
fatalError: make(chan error, 1),
|
||||
|
||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
||||
if isGit {
|
||||
return "dms-shell-git"
|
||||
}
|
||||
return "dms-shell-bin"
|
||||
return "dms-shell"
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||
if isGit {
|
||||
return "dms-git"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
dmsPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -10,7 +9,7 @@ let
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
dmsPkgs.dms-shell
|
||||
cfg.package
|
||||
]
|
||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||
++ lib.optionals cfg.enableVPN [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
let
|
||||
inherit (lib) types;
|
||||
cfg = config.programs.dank-material-shell.greeter;
|
||||
cfgDms = config.programs.dank-material-shell;
|
||||
|
||||
inherit (config.services.greetd.settings.default_session) user;
|
||||
|
||||
@@ -30,13 +31,13 @@ let
|
||||
lib.escapeShellArgs (
|
||||
[
|
||||
"sh"
|
||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
||||
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||
"--cache-dir"
|
||||
cacheDir
|
||||
"--command"
|
||||
cfg.compositor.name
|
||||
"-p"
|
||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||
"${cfg.package}/share/quickshell/dms"
|
||||
]
|
||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||
"-C"
|
||||
@@ -66,6 +67,21 @@ in
|
||||
|
||||
options.programs.dank-material-shell.greeter = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.programs.dank-material-shell.enable
|
||||
then config.programs.dank-material-shell.package
|
||||
else built from source;
|
||||
'';
|
||||
description = ''
|
||||
The DankMaterialShell package to use for the greeter.
|
||||
|
||||
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
||||
otherwise defaults to building from source.
|
||||
'';
|
||||
};
|
||||
compositor.name = lib.mkOption {
|
||||
type = types.enum [
|
||||
"niri"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -13,7 +12,6 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||
@@ -96,7 +94,7 @@ in
|
||||
};
|
||||
|
||||
Service = {
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -12,7 +11,6 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
in
|
||||
@@ -36,7 +34,7 @@ in
|
||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ in
|
||||
|
||||
options.programs.dank-material-shell = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell";
|
||||
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
||||
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
||||
};
|
||||
|
||||
systemd = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
@@ -13,21 +14,31 @@ Singleton {
|
||||
signal popoutChanged
|
||||
|
||||
function _closePopout(popout) {
|
||||
switch (true) {
|
||||
case popout.dashVisible !== undefined:
|
||||
popout.dashVisible = false;
|
||||
try {
|
||||
switch (true) {
|
||||
case popout.dashVisible !== undefined:
|
||||
popout.dashVisible = false;
|
||||
return;
|
||||
case popout.notificationHistoryVisible !== undefined:
|
||||
popout.notificationHistoryVisible = false;
|
||||
return;
|
||||
default:
|
||||
if (typeof popout.close !== "function")
|
||||
return;
|
||||
popout.close();
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
case popout.notificationHistoryVisible !== undefined:
|
||||
popout.notificationHistoryVisible = false;
|
||||
return;
|
||||
default:
|
||||
popout.close();
|
||||
}
|
||||
}
|
||||
|
||||
function _isStale(popout) {
|
||||
try {
|
||||
return !popout || !("shouldBeVisible" in popout);
|
||||
if (!popout || !("shouldBeVisible" in popout))
|
||||
return true;
|
||||
if (!popout.screen)
|
||||
return true;
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -70,14 +70,12 @@ Singleton {
|
||||
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||
}
|
||||
|
||||
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
|
||||
pamFprintSupportDetected = false;
|
||||
pamU2fSupportDetected = false;
|
||||
pamSupportProbeOutput = "";
|
||||
pamSupportProbeStreamFinished = false;
|
||||
pamSupportProbeExited = false;
|
||||
pamSupportDetectionProcess.running = true;
|
||||
}
|
||||
pamFprintSupportDetected = false;
|
||||
pamU2fSupportDetected = false;
|
||||
pamSupportProbeOutput = "";
|
||||
pamSupportProbeStreamFinished = false;
|
||||
pamSupportProbeExited = false;
|
||||
pamSupportDetectionProcess.running = true;
|
||||
|
||||
recomputeAuthCapabilities();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Greetd
|
||||
import qs.Common
|
||||
import qs.Modules.Greetd
|
||||
|
||||
Scope {
|
||||
|
||||
@@ -129,7 +129,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -195,7 +195,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -60,15 +60,12 @@ DankModal {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
shouldHaveFocus = true;
|
||||
ClipboardService.reset();
|
||||
ClipboardService.refresh();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
|
||||
@@ -50,14 +50,11 @@ DankPopout {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
ClipboardService.reset();
|
||||
ClipboardService.refresh();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
@@ -122,10 +119,10 @@ DankPopout {
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
if (!shouldBeVisible)
|
||||
return;
|
||||
}
|
||||
ClipboardService.refresh();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item?.searchField) {
|
||||
|
||||
@@ -225,7 +225,13 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success")
|
||||
text: {
|
||||
if (root.errorCount === 0)
|
||||
return I18n.tr("All checks passed", "greeter doctor page success");
|
||||
return root.errorCount === 1
|
||||
? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
|
||||
: I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -107,6 +107,26 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: blurWallpaperWindow
|
||||
function onWidthChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: renderSettleTimer
|
||||
interval: 1000
|
||||
|
||||
@@ -448,7 +448,7 @@ Variants {
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5);
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
|
||||
}
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
}
|
||||
|
||||
@@ -230,19 +230,19 @@ Item {
|
||||
|
||||
function currentAuthMessage() {
|
||||
if (GreeterState.pamState === "error")
|
||||
return "Authentication error - try again";
|
||||
return I18n.tr("Authentication error - try again");
|
||||
if (GreeterState.pamState === "max")
|
||||
return "Too many failed attempts - account may be locked";
|
||||
return I18n.tr("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 I18n.tr("Incorrect password - attempt %1 of %2 (lockout may follow)").arg(attempt).arg(passwordAttemptLimitHint);
|
||||
}
|
||||
return "Incorrect password - next failures may trigger account lockout";
|
||||
return I18n.tr("Incorrect password - next failures may trigger account lockout");
|
||||
}
|
||||
return "Incorrect password";
|
||||
return I18n.tr("Incorrect password");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -767,7 +767,7 @@ Item {
|
||||
|
||||
property string fullTimeStr: {
|
||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
|
||||
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||
}
|
||||
property var timeParts: fullTimeStr.split(':')
|
||||
property string hours: timeParts[0] || ""
|
||||
@@ -876,7 +876,7 @@ Item {
|
||||
anchors.top: clockContainer.bottom
|
||||
anchors.topMargin: 4
|
||||
text: {
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
@@ -1012,15 +1012,15 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (GreeterState.unlocking) {
|
||||
return "Logging in...";
|
||||
return I18n.tr("Logging in...");
|
||||
}
|
||||
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||
return "Authenticating...";
|
||||
return I18n.tr("Authenticating...");
|
||||
}
|
||||
if (GreeterState.showPasswordInput) {
|
||||
return "Password...";
|
||||
return I18n.tr("Password...");
|
||||
}
|
||||
return "Username...";
|
||||
return I18n.tr("Username...");
|
||||
}
|
||||
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
|
||||
@@ -6,7 +6,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
|
||||
|
||||
- **Multi user**: Login with any system user
|
||||
- **dms sync**: Sync settings with dms for consistent styling between shell and greeter
|
||||
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
||||
- **Multiple compositors**: The `dms-greeter` wrapper supports niri, Hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
|
||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||
- **Session Memory**: Remembers last selected session and user
|
||||
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||
@@ -152,8 +152,8 @@ sudo chmod +x /usr/local/bin/dms-greeter
|
||||
5. Create greeter cache directory with proper permissions:
|
||||
```bash
|
||||
sudo mkdir -p /var/cache/dms-greeter
|
||||
sudo chown greeter:greeter /var/cache/dms-greeter
|
||||
sudo chmod 750 /var/cache/dms-greeter
|
||||
sudo chown <greeter-user>:<greeter-group> /var/cache/dms-greeter
|
||||
sudo chmod 2770 /var/cache/dms-greeter
|
||||
```
|
||||
|
||||
6. Edit or create `/etc/greetd/config.toml`:
|
||||
@@ -163,7 +163,7 @@ vt = 1
|
||||
|
||||
[default_session]
|
||||
user = "greeter"
|
||||
# Change compositor to sway, hyprland, or mangowc if preferred
|
||||
# Change compositor to another wrapper-supported compositor if preferred
|
||||
command = "/usr/local/bin/dms-greeter --command niri"
|
||||
```
|
||||
|
||||
@@ -238,9 +238,12 @@ DMS_RUN_GREETER=1 qs -p /path/to/dms
|
||||
|
||||
#### Compositor
|
||||
|
||||
You can configure compositor specific settings such as outputs/displays the same as you would in niri or Hyprland.
|
||||
For current wrapper-based installs, the `dms-greeter` wrapper supports niri, hyprland, sway, scroll, miracle-wm, labwc, and mangowc.
|
||||
|
||||
Simply edit `/etc/greetd/dms-niri.kdl` or `/etc/greetd/dms-hypr.conf` to change compositor settings for the greeter
|
||||
Only niri currently has a generated greeter config path managed by `dms greeter sync`.
|
||||
|
||||
- niri: `dms greeter sync` writes the generated greeter config to `/etc/greetd/niri/config.kdl`. Add local manual tweaks in `/etc/greetd/niri_overrides.kdl`.
|
||||
- Other wrapper-supported compositors use the wrapper-generated config by default. If you need a custom compositor config, add `-C /path/to/config` to the `dms-greeter` command in `/etc/greetd/config.toml`.
|
||||
|
||||
#### Personalization
|
||||
|
||||
@@ -271,4 +274,4 @@ sudo ln -sf ~/.cache/DankMaterialShell/dms-colors.json /var/cache/dms-greeter/co
|
||||
|
||||
**Advanced:** You can override the configuration path with the `DMS_GREET_CFG_DIR` environment variable or the `--cache-dir` flag when using `dms-greeter`. The default is `/var/cache/dms-greeter`.
|
||||
|
||||
The cache directory should be owned by `greeter:greeter` with `770` permissions.
|
||||
The cache directory should be owned by `<greeter-user>:<greeter-group>` with `2770` permissions. If the greeter user is not available yet, DMS falls back to `root:<greeter-group>`.
|
||||
|
||||
@@ -214,18 +214,6 @@ 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
|
||||
|
||||
@@ -39,6 +39,38 @@ Item {
|
||||
lockerReadyArmed = true;
|
||||
unlocking = false;
|
||||
pamState = "";
|
||||
if (pam)
|
||||
pam.lockMessage = "";
|
||||
}
|
||||
|
||||
function currentAuthFeedbackText() {
|
||||
if (!pam)
|
||||
return "";
|
||||
if (pam.u2fState === "insert" && !pam.u2fPending)
|
||||
return I18n.tr("Insert your security key...");
|
||||
if (pam.u2fState === "waiting" && !pam.u2fPending)
|
||||
return I18n.tr("Touch your security key...");
|
||||
if (pam.lockMessage && pam.lockMessage.length > 0)
|
||||
return pam.lockMessage;
|
||||
if (pam.fprintState === "error") {
|
||||
const detail = (pam.fprint.message || "").trim();
|
||||
return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
|
||||
}
|
||||
if (pam.fprintState === "max")
|
||||
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
|
||||
if (pam.fprintState === "fail")
|
||||
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
|
||||
if (root.pamState === "error")
|
||||
return I18n.tr("Authentication error - try again");
|
||||
if (root.pamState === "max")
|
||||
return I18n.tr("Too many attempts - locked out");
|
||||
if (root.pamState === "fail")
|
||||
return I18n.tr("Incorrect password - try again");
|
||||
return "";
|
||||
}
|
||||
|
||||
function authFeedbackIsHint() {
|
||||
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -333,9 +365,9 @@ Item {
|
||||
visible: SettingsData.lockScreenShowDate
|
||||
text: {
|
||||
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
|
||||
}
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
@@ -687,14 +719,24 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (pam.u2fPending)
|
||||
return "passkey";
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return "fingerprint_off";
|
||||
if (pam.fprint.active)
|
||||
return "fingerprint";
|
||||
if (pam.u2f.active)
|
||||
return "passkey";
|
||||
return "lock";
|
||||
}
|
||||
size: 20
|
||||
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
|
||||
color: {
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return Theme.error;
|
||||
if (pam.u2fState !== "")
|
||||
return Theme.tertiary;
|
||||
return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
|
||||
}
|
||||
opacity: pam.passwd.active ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -760,6 +802,11 @@ Item {
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (pam.u2fPending) {
|
||||
pam.cancelU2fPending();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
@@ -824,6 +871,11 @@ Item {
|
||||
if (root.unlocking) {
|
||||
return "Unlocking...";
|
||||
}
|
||||
if (pam.u2fPending) {
|
||||
if (pam.u2fState === "insert")
|
||||
return "Insert your security key...";
|
||||
return "Touch your security key...";
|
||||
}
|
||||
if (pam.passwd.active) {
|
||||
return "Authenticating...";
|
||||
}
|
||||
@@ -898,7 +950,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: !demoMode && !pam.passwd.active && !root.unlocking
|
||||
visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboardController.isKeyboardActive) {
|
||||
@@ -999,7 +1051,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||
enabled: !demoMode
|
||||
onClicked: {
|
||||
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||
@@ -1025,24 +1077,18 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: authFeedbackText
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 20
|
||||
text: {
|
||||
if (root.pamState === "error") {
|
||||
return "Authentication error - try again";
|
||||
}
|
||||
if (root.pamState === "max") {
|
||||
return "Too many attempts - locked out";
|
||||
}
|
||||
if (root.pamState === "fail") {
|
||||
return "Incorrect password - try again";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: Theme.error
|
||||
Layout.preferredHeight: text.length > 0 ? Math.min(implicitHeight, Math.ceil(Theme.fontSizeSmall * 4.5)) : 0
|
||||
text: root.currentAuthFeedbackText()
|
||||
color: root.authFeedbackIsHint() ? Theme.outline : Theme.error
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: root.pamState !== "" ? 1 : 0
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
opacity: text.length > 0 ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
@@ -1611,6 +1657,14 @@ Item {
|
||||
root.passwordBuffer = "";
|
||||
}
|
||||
}
|
||||
onU2fPendingChanged: {
|
||||
if (u2fPending) {
|
||||
passwordField.text = "";
|
||||
root.passwordBuffer = "";
|
||||
if (keyboardController.isKeyboardActive)
|
||||
keyboardController.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -14,9 +14,12 @@ Scope {
|
||||
|
||||
readonly property alias passwd: passwd
|
||||
readonly property alias fprint: fprint
|
||||
readonly property alias u2f: u2f
|
||||
property string lockMessage
|
||||
property string state
|
||||
property string fprintState
|
||||
property string u2fState
|
||||
property bool u2fPending: false
|
||||
property string buffer
|
||||
|
||||
signal flashMsg
|
||||
@@ -31,14 +34,14 @@ Scope {
|
||||
u2fPendingTimeout.running = false;
|
||||
passwdActiveTimeout.running = false;
|
||||
unlockRequestTimeout.running = false;
|
||||
u2fPending = false;
|
||||
u2fState = "";
|
||||
unlockInProgress = false;
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
root.unlockInProgress = false;
|
||||
}
|
||||
|
||||
function recoverFromAuthStall(newState: string): void {
|
||||
resetAuthFlows();
|
||||
state = newState;
|
||||
root.state = newState;
|
||||
flashMsg();
|
||||
stateReset.restart();
|
||||
fprint.checkAvail();
|
||||
@@ -46,16 +49,16 @@ Scope {
|
||||
}
|
||||
|
||||
function completeUnlock(): void {
|
||||
if (!unlockInProgress) {
|
||||
unlockInProgress = true;
|
||||
if (!root.unlockInProgress) {
|
||||
root.unlockInProgress = true;
|
||||
passwd.abort();
|
||||
fprint.abort();
|
||||
u2f.abort();
|
||||
errorRetry.running = false;
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
u2fPending = false;
|
||||
u2fState = "";
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
unlockRequestTimeout.restart();
|
||||
unlockRequested();
|
||||
}
|
||||
@@ -70,13 +73,13 @@ Scope {
|
||||
}
|
||||
|
||||
function cancelU2fPending(): void {
|
||||
if (!u2fPending)
|
||||
if (!root.u2fPending)
|
||||
return;
|
||||
u2f.abort();
|
||||
u2fErrorRetry.running = false;
|
||||
u2fPendingTimeout.running = false;
|
||||
u2fPending = false;
|
||||
u2fState = "";
|
||||
root.u2fPending = false;
|
||||
root.u2fState = "";
|
||||
fprint.checkAvail();
|
||||
}
|
||||
|
||||
@@ -94,17 +97,27 @@ Scope {
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: u2fConfigWatcher
|
||||
|
||||
path: "/etc/pam.d/dankshell-u2f"
|
||||
printErrors: false
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: passwd
|
||||
|
||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||
configDirectory: (dankshellConfigWatcher.loaded || loginConfigWatcher.loaded) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||
|
||||
onMessageChanged: {
|
||||
if (message.startsWith("The account is locked"))
|
||||
if (message.startsWith("The account is locked")) {
|
||||
root.lockMessage = message;
|
||||
else if (root.lockMessage && message.endsWith(" left to unlock)"))
|
||||
} else if (root.lockMessage && message.endsWith(" left to unlock)")) {
|
||||
root.lockMessage += "\n" + message;
|
||||
} else if (root.lockMessage && message && message.length > 0) {
|
||||
root.lockMessage = "";
|
||||
}
|
||||
}
|
||||
|
||||
onResponseRequiredChanged: {
|
||||
@@ -117,9 +130,8 @@ Scope {
|
||||
onCompleted: res => {
|
||||
if (res === PamResult.Success) {
|
||||
if (!root.unlockInProgress) {
|
||||
root.unlockInProgress = true;
|
||||
fprint.abort();
|
||||
root.unlockRequested();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -182,9 +194,8 @@ Scope {
|
||||
|
||||
if (res === PamResult.Success) {
|
||||
if (!root.unlockInProgress) {
|
||||
root.unlockInProgress = true;
|
||||
passwd.abort();
|
||||
root.unlockRequested();
|
||||
root.proceedAfterPrimaryAuth();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -244,7 +255,7 @@ Scope {
|
||||
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||
|
||||
onMessageChanged: {
|
||||
if (message !== "")
|
||||
if (message.toLowerCase().includes("touch"))
|
||||
root.u2fState = "waiting";
|
||||
}
|
||||
|
||||
@@ -346,6 +357,8 @@ Scope {
|
||||
SettingsData.refreshAuthAvailability();
|
||||
root.state = "";
|
||||
root.fprintState = "";
|
||||
root.u2fState = "";
|
||||
root.u2fPending = false;
|
||||
root.lockMessage = "";
|
||||
root.resetAuthFlows();
|
||||
fprint.checkAvail();
|
||||
|
||||
@@ -108,10 +108,32 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: wallpaperWindow
|
||||
function onWidthChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onDisplayScalesChanged() {
|
||||
root._recheckScreenScale();
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +141,8 @@ Variants {
|
||||
target: WlrOutputService
|
||||
function onWlrOutputAvailableChanged() {
|
||||
root._recheckScreenScale();
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,12 @@ Singleton {
|
||||
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
|
||||
}
|
||||
|
||||
onClipboardAvailableChanged: {
|
||||
if (!clipboardAvailable || refCount <= 0)
|
||||
return;
|
||||
refresh();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: root.refCount > 0
|
||||
|
||||
@@ -3,7 +3,7 @@ import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Flow {
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property var model: []
|
||||
|
||||
3
quickshell/assets/pam/u2f
Normal file
3
quickshell/assets/pam/u2f
Normal file
@@ -0,0 +1,3 @@
|
||||
#%PAM-1.0
|
||||
|
||||
auth required pam_u2f.so cue nouserok timeout=10
|
||||
Reference in New Issue
Block a user