mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 10:32:07 -04:00
Compare commits
87 Commits
e7c8d208e2
...
frame
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1217b25de5 | ||
|
|
e913630f90 | ||
|
|
220bb2708b | ||
|
|
e57ab3e1f3 | ||
|
|
952ab9b753 | ||
|
|
28f9aabcd9 | ||
|
|
3d9bd73336 | ||
|
|
3497d5f523 | ||
|
|
8ef1d95e65 | ||
|
|
e9aeb9ac60 | ||
|
|
fb02f7294d | ||
|
|
f15d49d80a | ||
|
|
c471cff456 | ||
|
|
f83bb10e0c | ||
|
|
74ad58b1e1 | ||
|
|
577863b969 | ||
|
|
03d2a3fd39 | ||
|
|
802b23ed60 | ||
|
|
2b9f3a9eef | ||
|
|
62c60900eb | ||
|
|
b381e1e54c | ||
|
|
e7ee26ce74 | ||
|
|
521a3fa6e8 | ||
|
|
5ee93a67fe | ||
|
|
5d0a03c822 | ||
|
|
293c2a0035 | ||
|
|
9a5fa50541 | ||
|
|
d5ceea8a56 | ||
|
|
faa5e7e02d | ||
|
|
516c478f3d | ||
|
|
906c6a2501 | ||
|
|
86d8fe4fa4 | ||
|
|
9b44bc3259 | ||
|
|
59b6d2237b | ||
|
|
7e559cc0bb | ||
|
|
fd1facfce8 | ||
|
|
8f26193cc3 | ||
|
|
43b2e5315d | ||
|
|
5cad89e9cc | ||
|
|
3804d2f00b | ||
|
|
4d649468d5 | ||
|
|
c5f145be36 | ||
|
|
76dff870a7 | ||
|
|
6c8d3fc007 | ||
|
|
e7ffa23016 | ||
|
|
4266c064a9 | ||
|
|
5f631b36cd | ||
|
|
be8326f497 | ||
|
|
07dbba6c53 | ||
|
|
a53b9afb44 | ||
|
|
a0c7ffd6b9 | ||
|
|
7ca1d2325a | ||
|
|
8d0f256f74 | ||
|
|
1a9449da1b | ||
|
|
1caf8942b7 | ||
|
|
9efbcbcd20 | ||
|
|
3d07b8c9c1 | ||
|
|
dae74a40c0 | ||
|
|
959190dcbc | ||
|
|
1e48976ae5 | ||
|
|
0a8c111e12 | ||
|
|
19c786c0be | ||
|
|
7f8b260560 | ||
|
|
368536f698 | ||
|
|
b227221df6 | ||
|
|
8e047f45f5 | ||
|
|
fbe8cbb23f | ||
|
|
28315a165f | ||
|
|
1b32829dac | ||
|
|
1fce29324f | ||
|
|
1fab90178a | ||
|
|
eb04ab7dca | ||
|
|
e9fa2c78ee | ||
|
|
59dae954cd | ||
|
|
5c4ce86da4 | ||
|
|
0cf2c40377 | ||
|
|
679a59ad76 | ||
|
|
db3209afbe | ||
|
|
f0be36062e | ||
|
|
9578d6daf9 | ||
|
|
cc6766135d | ||
|
|
28c9bb0925 | ||
|
|
7826d827dd | ||
|
|
7f392acc54 | ||
|
|
190fd662ad | ||
|
|
e18587c471 | ||
|
|
ddb079b62d |
6
.github/workflows/run-ppa.yml
vendored
6
.github/workflows/run-ppa.yml
vendored
@@ -242,7 +242,11 @@ jobs:
|
||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||
# ppa-upload.sh uploads to questing + resolute when series is omitted
|
||||
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
|
||||
echo "::error::Upload failed for $PKG"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Summary
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
This file is more of a quick reference so I know what to account for before next releases.
|
||||
|
||||
# 1.5.0
|
||||
- Overhauled shadows
|
||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||
- Greeter stuff
|
||||
- Terminal mux
|
||||
- Locale overrides
|
||||
- new neovim theming
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Overhauled system monitor, graphs, styling
|
||||
|
||||
76
core/cmd/dms/commands_auth.go
Normal file
76
core/cmd/dms/commands_auth.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var authCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Manage DMS authentication sync",
|
||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
||||
}
|
||||
|
||||
var authSyncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync DMS authentication configuration",
|
||||
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
yes, _ := cmd.Flags().GetBool("yes")
|
||||
term, _ := cmd.Flags().GetBool("terminal")
|
||||
if term {
|
||||
if err := syncAuthInTerminal(yes); err != nil {
|
||||
log.Fatalf("Error launching auth sync in terminal: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := syncAuth(yes); err != nil {
|
||||
log.Fatalf("Error syncing authentication: %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||
}
|
||||
|
||||
func syncAuth(nonInteractive bool) error {
|
||||
if !nonInteractive {
|
||||
fmt.Println("=== DMS Authentication Sync ===")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
logFunc := func(msg string) {
|
||||
fmt.Println(msg)
|
||||
}
|
||||
|
||||
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !nonInteractive {
|
||||
fmt.Println("\n=== Authentication Sync Complete ===")
|
||||
fmt.Println("\nAuthentication changes have been applied.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncAuthInTerminal(nonInteractive bool) error {
|
||||
syncFlags := make([]string, 0, 1)
|
||||
if nonInteractive {
|
||||
syncFlags = append(syncFlags, "--yes")
|
||||
}
|
||||
|
||||
shellSyncCmd := "dms auth sync"
|
||||
if len(syncFlags) > 0 {
|
||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||
}
|
||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||
return runCommandInTerminal(shellCmd)
|
||||
}
|
||||
40
core/cmd/dms/commands_blur.go
Normal file
40
core/cmd/dms/commands_blur.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var blurCmd = &cobra.Command{
|
||||
Use: "blur",
|
||||
Short: "Background blur utilities",
|
||||
}
|
||||
|
||||
var blurCheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||
Args: cobra.NoArgs,
|
||||
Run: runBlurCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
blurCmd.AddCommand(blurCheckCmd)
|
||||
}
|
||||
|
||||
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||
supported, err := blur.ProbeSupport()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch supported {
|
||||
case true:
|
||||
fmt.Println("supported")
|
||||
default:
|
||||
fmt.Println("unsupported")
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ Output format flags (mutually exclusive, default: --hex):
|
||||
--cmyk - CMYK values (C% M% Y% K%)
|
||||
--json - JSON with all formats
|
||||
|
||||
Optional:
|
||||
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
||||
|
||||
Examples:
|
||||
dms color pick # Pick color, output as hex
|
||||
dms color pick --rgb # Output as RGB
|
||||
@@ -53,6 +56,7 @@ func init() {
|
||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
|
||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||
@@ -113,7 +117,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
||||
|
||||
if jsonOutput {
|
||||
fmt.Println(output)
|
||||
} else if color.IsDark() {
|
||||
return
|
||||
}
|
||||
|
||||
if raw, _ := cmd.Flags().GetBool("raw"); raw {
|
||||
fmt.Printf("%s\n", output)
|
||||
return
|
||||
}
|
||||
|
||||
if color.IsDark() {
|
||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||
} else {
|
||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||
|
||||
@@ -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
|
||||
@@ -526,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
blurCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
@@ -25,6 +26,11 @@ var greeterCmd = &cobra.Command{
|
||||
Long: "Manage DMS greeter (greetd)",
|
||||
}
|
||||
|
||||
var (
|
||||
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
||||
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
||||
)
|
||||
|
||||
var greeterInstallCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install and configure DMS greeter",
|
||||
@@ -148,6 +154,16 @@ func init() {
|
||||
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
|
||||
}
|
||||
|
||||
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
|
||||
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if beforeAuth != nil {
|
||||
beforeAuth()
|
||||
}
|
||||
return sharedAuthSyncFn(logFunc, "", options)
|
||||
}
|
||||
|
||||
func installGreeter(nonInteractive bool) error {
|
||||
fmt.Println("=== DMS Greeter Installation ===")
|
||||
|
||||
@@ -243,7 +259,9 @@ func installGreeter(nonInteractive bool) error {
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
|
||||
if err := syncGreeterConfigsAndAuth(dmsPath, selectedCompositor, logFunc, sharedpam.SyncAuthOptions{}, func() {
|
||||
fmt.Println("\nConfiguring authentication...")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -278,7 +296,7 @@ func uninstallGreeter(nonInteractive bool) error {
|
||||
}
|
||||
|
||||
if !nonInteractive {
|
||||
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS PAM managed block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
|
||||
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS-managed greeter auth block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||
@@ -297,8 +315,8 @@ func uninstallGreeter(nonInteractive bool) error {
|
||||
fmt.Println(" ✓ greetd disabled")
|
||||
}
|
||||
|
||||
fmt.Println("\nRemoving DMS PAM configuration...")
|
||||
if err := greeter.RemoveGreeterPamManagedBlock(logFunc, ""); err != nil {
|
||||
fmt.Println("\nRemoving DMS authentication configuration...")
|
||||
if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil {
|
||||
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -535,7 +553,7 @@ func resolveLocalWrapperShell() (string, error) {
|
||||
|
||||
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
if !nonInteractive {
|
||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||
fmt.Println("=== DMS Greeter Sync ===")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
@@ -721,7 +739,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
||||
if err := syncGreeterConfigsAndAuth(dmsPath, compositor, logFunc, sharedpam.SyncAuthOptions{
|
||||
ForceGreeterAuth: forceAuth,
|
||||
}, func() {
|
||||
fmt.Println("\nConfiguring authentication...")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -734,8 +756,9 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
|
||||
fmt.Println("\n=== Sync Complete ===")
|
||||
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||
fmt.Println("Shared authentication settings were also checked and reconciled where needed.")
|
||||
if forceAuth {
|
||||
fmt.Println("PAM has been configured for fingerprint and U2F (where modules exist).")
|
||||
fmt.Println("Authentication has been configured for fingerprint and U2F (where modules exist).")
|
||||
}
|
||||
fmt.Println("The changes will be visible on the next login screen.")
|
||||
|
||||
@@ -1297,39 +1320,7 @@ func extractGreeterPathOverrideFromCommand(command string) string {
|
||||
}
|
||||
|
||||
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||
if pamText == "" {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
lines := strings.Split(pamText, "\n")
|
||||
inManaged := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch trimmed {
|
||||
case greeter.GreeterPamManagedBlockStart:
|
||||
managed = true
|
||||
inManaged = true
|
||||
continue
|
||||
case greeter.GreeterPamManagedBlockEnd:
|
||||
inManaged = false
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "# DMS greeter fingerprint") || strings.HasPrefix(trimmed, "# DMS greeter U2F") {
|
||||
legacy = true
|
||||
}
|
||||
if !inManaged {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_fprintd") {
|
||||
fingerprint = true
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_u2f") {
|
||||
u2f = true
|
||||
}
|
||||
}
|
||||
|
||||
return managed, fingerprint, u2f, legacy
|
||||
return sharedpam.ParseManagedGreeterPamAuth(pamText)
|
||||
}
|
||||
|
||||
func packageInstallHint() string {
|
||||
@@ -1490,6 +1481,19 @@ func checkGreeterStatus() error {
|
||||
}
|
||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||
requiredSubdirs := []string{".local/state", ".local/share", ".cache"}
|
||||
missingSubdirs := false
|
||||
for _, sub := range requiredSubdirs {
|
||||
subPath := filepath.Join(cacheDir, sub)
|
||||
if _, err := os.Stat(subPath); os.IsNotExist(err) {
|
||||
fmt.Printf(" ⚠ Missing required subdir: %s\n", subPath)
|
||||
missingSubdirs = true
|
||||
}
|
||||
}
|
||||
if missingSubdirs {
|
||||
fmt.Println(" Run 'dms greeter sync' to initialize the cache directory structure.")
|
||||
allGood = false
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||
fmt.Printf(" %s\n", packageInstallHint())
|
||||
@@ -1497,6 +1501,20 @@ func checkGreeterStatus() error {
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguration Symlinks:")
|
||||
colorSyncInfo, colorSyncErr := greeter.ResolveGreeterColorSyncInfo(homeDir)
|
||||
if colorSyncErr != nil {
|
||||
fmt.Printf(" ✗ Failed to resolve expected greeter color source: %v\n", colorSyncErr)
|
||||
allGood = false
|
||||
colorSyncInfo = greeter.GreeterColorSyncInfo{
|
||||
SourcePath: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
}
|
||||
}
|
||||
|
||||
colorThemeDesc := "Color theme"
|
||||
if colorSyncInfo.UsesDynamicWallpaperOverride {
|
||||
colorThemeDesc = "Color theme (greeter wallpaper override)"
|
||||
}
|
||||
|
||||
symlinks := []struct {
|
||||
source string
|
||||
target string
|
||||
@@ -1513,9 +1531,9 @@ func checkGreeterStatus() error {
|
||||
desc: "Session state",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
source: colorSyncInfo.SourcePath,
|
||||
target: filepath.Join(cacheDir, "colors.json"),
|
||||
desc: "Color theme",
|
||||
desc: colorThemeDesc,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1557,6 +1575,10 @@ func checkGreeterStatus() error {
|
||||
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||
}
|
||||
|
||||
if colorSyncInfo.UsesDynamicWallpaperOverride {
|
||||
fmt.Printf(" ℹ Dynamic theme uses greeter override colors from %s\n", colorSyncInfo.SourcePath)
|
||||
}
|
||||
|
||||
fmt.Println("\nGreeter Wallpaper Override:")
|
||||
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
||||
@@ -1608,29 +1630,29 @@ func checkGreeterStatus() error {
|
||||
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
||||
}
|
||||
if legacyManaged {
|
||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
||||
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms auth sync' to normalize.")
|
||||
allGood = false
|
||||
}
|
||||
enableFprintToggle, enableU2fToggle := false, false
|
||||
if enableFprint, enableU2f, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil {
|
||||
if enableFprint, enableU2f, settingsErr := sharedpam.ReadGreeterAuthToggles(homeDir); settingsErr == nil {
|
||||
enableFprintToggle = enableFprint
|
||||
enableU2fToggle = enableU2f
|
||||
} else {
|
||||
fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr)
|
||||
}
|
||||
|
||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||
includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
||||
fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser()
|
||||
includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||
includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
||||
fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser()
|
||||
|
||||
if managedFprint && includedFprintFile != "" {
|
||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
|
||||
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.")
|
||||
fmt.Println(" Double fingerprint auth detected — run 'dms auth sync' to resolve.")
|
||||
allGood = false
|
||||
}
|
||||
if managedU2f && includedU2fFile != "" {
|
||||
fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile)
|
||||
fmt.Println(" Double security-key auth detected — run 'dms greeter sync' to resolve.")
|
||||
fmt.Println(" Double security-key auth detected — run 'dms auth sync' to resolve.")
|
||||
allGood = false
|
||||
}
|
||||
|
||||
|
||||
87
core/cmd/dms/commands_greeter_test.go
Normal file
87
core/cmd/dms/commands_greeter_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||
)
|
||||
|
||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||
t.Cleanup(func() {
|
||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||
})
|
||||
|
||||
var calls []string
|
||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
if dmsPath != "/tmp/dms" {
|
||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
||||
}
|
||||
if compositor != "niri" {
|
||||
t.Fatalf("unexpected compositor %q", compositor)
|
||||
}
|
||||
if sudoPassword != "" {
|
||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||
}
|
||||
calls = append(calls, "configs")
|
||||
return nil
|
||||
}
|
||||
|
||||
var gotOptions sharedpam.SyncAuthOptions
|
||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
||||
if sudoPassword != "" {
|
||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||
}
|
||||
gotOptions = options
|
||||
calls = append(calls, "auth")
|
||||
return nil
|
||||
}
|
||||
|
||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
||||
ForceGreeterAuth: true,
|
||||
}, func() {
|
||||
calls = append(calls, "before-auth")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
||||
}
|
||||
|
||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
||||
if !reflect.DeepEqual(calls, wantCalls) {
|
||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
||||
}
|
||||
if !gotOptions.ForceGreeterAuth {
|
||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||
t.Cleanup(func() {
|
||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||
})
|
||||
|
||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
||||
return errors.New("config sync failed")
|
||||
}
|
||||
|
||||
authCalled := false
|
||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
||||
authCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
||||
if err == nil || err.Error() != "config sync failed" {
|
||||
t.Fatalf("expected config sync error, got %v", err)
|
||||
}
|
||||
if authCalled {
|
||||
t.Fatal("expected auth sync not to run after config sync failure")
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ func init() {
|
||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||
}
|
||||
|
||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||
@@ -77,6 +78,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||
|
||||
return matugen.Options{
|
||||
StateDir: stateDir,
|
||||
@@ -87,6 +89,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
Mode: matugen.ColorMode(mode),
|
||||
IconTheme: iconTheme,
|
||||
MatugenType: matugenType,
|
||||
Contrast: contrast,
|
||||
RunUserTemplates: runUserTemplates,
|
||||
StockColors: stockColors,
|
||||
SyncModeWithPortal: syncModeWithPortal,
|
||||
@@ -128,6 +131,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||
"skipTemplates": opts.SkipTemplates,
|
||||
"contrast": opts.Contrast,
|
||||
"wait": wait,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ var (
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssNoConfirm bool
|
||||
ssReset bool
|
||||
ssStdout bool
|
||||
)
|
||||
|
||||
@@ -50,8 +52,10 @@ Examples:
|
||||
dms screenshot output -o DP-1 # Specific output
|
||||
dms screenshot window # Focused window (Hyprland)
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --reset # Reset last region pre-selection
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
dms screenshot --no-file # Clipboard only
|
||||
dms screenshot --no-confirm # Region capture on mouse release
|
||||
dms screenshot --cursor=on # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||
}
|
||||
@@ -119,6 +123,8 @@ func init() {
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||
|
||||
screenshotCmd.AddCommand(ssRegionCmd)
|
||||
@@ -142,6 +148,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config.Clipboard = !ssNoClipboard
|
||||
config.SaveFile = !ssNoFile
|
||||
config.Notify = !ssNoNotify
|
||||
config.NoConfirm = ssNoConfirm
|
||||
config.Reset = ssReset
|
||||
config.Stdout = ssStdout
|
||||
|
||||
if ssOutputDir != "" {
|
||||
|
||||
@@ -17,11 +17,13 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
authCmd.AddCommand(authSyncCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(authCmd)
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
|
||||
@@ -17,9 +17,11 @@ func init() {
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
authCmd.AddCommand(authSyncCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
rootCmd.AddCommand(authCmd)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
@@ -192,6 +192,9 @@ func runShellInteractive(session bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
@@ -432,6 +435,9 @@ func runShellDaemon(session bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||
|
||||
if isSessionManaged && hasSystemdRun() {
|
||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||
}
|
||||
@@ -616,6 +622,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 +670,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
|
||||
|
||||
35
core/internal/blur/probe.go
Normal file
35
core/internal/blur/probe.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package blur
|
||||
|
||||
import (
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||
|
||||
func ProbeSupport() (bool, error) {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer display.Context().Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
found := false
|
||||
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case extBackgroundEffectInterface:
|
||||
found = true
|
||||
}
|
||||
})
|
||||
|
||||
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return found, nil
|
||||
}
|
||||
@@ -52,35 +52,53 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||
|
||||
if stdinSource, ok := data.(*os.File); ok {
|
||||
cmd.Stdin = stdinSource
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
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)
|
||||
switch src := data.(type) {
|
||||
case *os.File:
|
||||
cmd.Stdin = src
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin 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)
|
||||
}
|
||||
}
|
||||
|
||||
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 signalReady() {
|
||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
||||
return
|
||||
}
|
||||
os.Stdout.Write([]byte{1})
|
||||
}
|
||||
|
||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
cachedData, err := createClipboardCacheFile()
|
||||
if err != nil {
|
||||
@@ -242,6 +260,7 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
signalReady()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -252,6 +252,7 @@ window-rule {
|
||||
// Open dms windows as floating by default
|
||||
window-rule {
|
||||
match app-id=r#"org.quickshell$"#
|
||||
match app-id=r#"com.danklinux.dms$"#
|
||||
open-floating true
|
||||
}
|
||||
debug {
|
||||
|
||||
@@ -135,6 +135,42 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||
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 {
|
||||
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 {
|
||||
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()
|
||||
if err != nil {
|
||||
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{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
||||
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||
IsComplete: false,
|
||||
CommandInfo: "Installing package dependencies and makedepends",
|
||||
CommandInfo: "Classifying dependencies as system or AUR",
|
||||
}
|
||||
|
||||
// Install dependencies from .SRCINFO
|
||||
depFilter := ""
|
||||
if pkg == "dms-shell-git" {
|
||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
||||
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||
}
|
||||
|
||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf(`
|
||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||
fi
|
||||
`, srcinfoPath, depFilter, sudoPassword))
|
||||
seen := make(map[string]bool)
|
||||
var systemPkgs []string
|
||||
var aurPkgs []string
|
||||
|
||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||
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",
|
||||
fmt.Sprintf(`
|
||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
||||
if [ ! -z "$makedeps" ]; then
|
||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
||||
fi
|
||||
`, srcinfoPath, sudoPassword))
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||
IsComplete: false,
|
||||
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 {
|
||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||
for _, aurDep := range aurPkgs {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
@@ -104,13 +103,15 @@ func debianPackageInstalledPrecisely(pkg string) bool {
|
||||
return strings.TrimSpace(string(output)) == "installed"
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
func debianRepoArchitecture(arch string) string {
|
||||
switch arch {
|
||||
case "amd64", "x86_64":
|
||||
return "amd64"
|
||||
case "arm64", "aarch64":
|
||||
return "arm64"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
@@ -460,7 +461,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
}
|
||||
|
||||
// Add repository
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||
@@ -425,7 +426,7 @@ func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string
|
||||
|
||||
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||
for _, pkg := range extraPkgs {
|
||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/sblinch/kdl-go"
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
@@ -23,26 +26,7 @@ var appArmorProfileData []byte
|
||||
|
||||
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
||||
|
||||
const (
|
||||
GreeterCacheDir = "/var/cache/dms-greeter"
|
||||
|
||||
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
||||
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
||||
|
||||
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
||||
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
||||
)
|
||||
|
||||
// Common PAM auth stack names referenced by greetd across supported distros.
|
||||
var includedPamAuthFiles = []string{
|
||||
"system-auth",
|
||||
"common-auth",
|
||||
"password-auth",
|
||||
"system-login",
|
||||
"system-local-login",
|
||||
"common-auth-pc",
|
||||
"login",
|
||||
}
|
||||
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||
|
||||
func DetectDMSPath() (string, error) {
|
||||
return config.LocateDMSConfig()
|
||||
@@ -628,7 +612,6 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||
runtimeDirs := []string{
|
||||
filepath.Join(cacheDir, ".local"),
|
||||
filepath.Join(cacheDir, ".local", "state"),
|
||||
filepath.Join(cacheDir, ".local", "state", "wireplumber"),
|
||||
filepath.Join(cacheDir, ".local", "share"),
|
||||
filepath.Join(cacheDir, ".cache"),
|
||||
}
|
||||
@@ -748,49 +731,6 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveGreeterPamManagedBlock strips the DMS managed auth block from /etc/pam.d/greetd
|
||||
func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) error {
|
||||
if IsNixOS() {
|
||||
return nil
|
||||
}
|
||||
const greetdPamPath = "/etc/pam.d/greetd"
|
||||
data, err := os.ReadFile(greetdPamPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
|
||||
}
|
||||
|
||||
stripped, removed := stripManagedGreeterPamBlock(string(data))
|
||||
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
||||
if !removed && !removedLegacy {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp("", "dms-pam-greetd-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp PAM file: %w", err)
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
if _, err := tmp.WriteString(strippedAgain); err != nil {
|
||||
tmp.Close()
|
||||
return fmt.Errorf("failed to write temp PAM file: %w", err)
|
||||
}
|
||||
tmp.Close()
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to write PAM config: %w", err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to set PAM config permissions: %w", err)
|
||||
}
|
||||
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor.
|
||||
// It is a no-op when AppArmor is not active or the profile does not exist.
|
||||
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
||||
@@ -1075,6 +1015,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
}{
|
||||
{filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
||||
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
||||
{filepath.Join(homeDir, ".cache", "DankMaterialShell"), "DankMaterialShell cache"},
|
||||
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
|
||||
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
||||
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
||||
@@ -1109,7 +1050,218 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
||||
type GreeterColorSyncInfo struct {
|
||||
SourcePath string
|
||||
ThemeName string
|
||||
UsesDynamicWallpaperOverride bool
|
||||
}
|
||||
|
||||
type greeterThemeSyncSettings struct {
|
||||
CurrentThemeName string `json:"currentThemeName"`
|
||||
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
|
||||
MatugenScheme string `json:"matugenScheme"`
|
||||
IconTheme string `json:"iconTheme"`
|
||||
}
|
||||
|
||||
type greeterThemeSyncSession struct {
|
||||
IsLightMode bool `json:"isLightMode"`
|
||||
}
|
||||
|
||||
type greeterThemeSyncState struct {
|
||||
ThemeName string
|
||||
GreeterWallpaperPath string
|
||||
ResolvedGreeterWallpaperPath string
|
||||
MatugenScheme string
|
||||
IconTheme string
|
||||
IsLightMode bool
|
||||
UsesDynamicWallpaperOverride bool
|
||||
}
|
||||
|
||||
func defaultGreeterColorsSource(homeDir string) string {
|
||||
return filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json")
|
||||
}
|
||||
|
||||
func greeterOverrideColorsStateDir(homeDir string) string {
|
||||
return filepath.Join(homeDir, ".cache", "DankMaterialShell", "greeter-colors")
|
||||
}
|
||||
|
||||
func greeterOverrideColorsSource(homeDir string) string {
|
||||
return filepath.Join(greeterOverrideColorsStateDir(homeDir), "dms-colors.json")
|
||||
}
|
||||
|
||||
func readOptionalJSONFile(path string, dst any) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == "" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, dst)
|
||||
}
|
||||
|
||||
func readGreeterThemeSyncSettings(homeDir string) (greeterThemeSyncSettings, error) {
|
||||
settings := greeterThemeSyncSettings{
|
||||
CurrentThemeName: "purple",
|
||||
MatugenScheme: "scheme-tonal-spot",
|
||||
IconTheme: "System Default",
|
||||
}
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
if err := readOptionalJSONFile(settingsPath, &settings); err != nil {
|
||||
return greeterThemeSyncSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func readGreeterThemeSyncSession(homeDir string) (greeterThemeSyncSession, error) {
|
||||
session := greeterThemeSyncSession{}
|
||||
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
|
||||
if err := readOptionalJSONFile(sessionPath, &session); err != nil {
|
||||
return greeterThemeSyncSession{}, fmt.Errorf("failed to parse session at %s: %w", sessionPath, err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func resolveGreeterThemeSyncState(homeDir string) (greeterThemeSyncState, error) {
|
||||
settings, err := readGreeterThemeSyncSettings(homeDir)
|
||||
if err != nil {
|
||||
return greeterThemeSyncState{}, err
|
||||
}
|
||||
session, err := readGreeterThemeSyncSession(homeDir)
|
||||
if err != nil {
|
||||
return greeterThemeSyncState{}, err
|
||||
}
|
||||
|
||||
resolvedWallpaperPath := ""
|
||||
if settings.GreeterWallpaperPath != "" {
|
||||
resolvedWallpaperPath = settings.GreeterWallpaperPath
|
||||
if !filepath.IsAbs(resolvedWallpaperPath) {
|
||||
resolvedWallpaperPath = filepath.Join(homeDir, resolvedWallpaperPath)
|
||||
}
|
||||
}
|
||||
|
||||
usesDynamicWallpaperOverride := strings.EqualFold(strings.TrimSpace(settings.CurrentThemeName), "dynamic") && resolvedWallpaperPath != ""
|
||||
|
||||
return greeterThemeSyncState{
|
||||
ThemeName: settings.CurrentThemeName,
|
||||
GreeterWallpaperPath: settings.GreeterWallpaperPath,
|
||||
ResolvedGreeterWallpaperPath: resolvedWallpaperPath,
|
||||
MatugenScheme: settings.MatugenScheme,
|
||||
IconTheme: settings.IconTheme,
|
||||
IsLightMode: session.IsLightMode,
|
||||
UsesDynamicWallpaperOverride: usesDynamicWallpaperOverride,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s greeterThemeSyncState) effectiveColorsSource(homeDir string) string {
|
||||
if s.UsesDynamicWallpaperOverride {
|
||||
return greeterOverrideColorsSource(homeDir)
|
||||
}
|
||||
return defaultGreeterColorsSource(homeDir)
|
||||
}
|
||||
|
||||
func ResolveGreeterColorSyncInfo(homeDir string) (GreeterColorSyncInfo, error) {
|
||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||
if err != nil {
|
||||
return GreeterColorSyncInfo{}, err
|
||||
}
|
||||
return GreeterColorSyncInfo{
|
||||
SourcePath: state.effectiveColorsSource(homeDir),
|
||||
ThemeName: state.ThemeName,
|
||||
UsesDynamicWallpaperOverride: state.UsesDynamicWallpaperOverride,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureGreeterSyncSourceFile(path string) error {
|
||||
sourceDir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create source directory %s: %w", sourceDir, err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(path, []byte("{}"), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to create source file %s: %w", path, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to inspect source file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncGreeterDynamicOverrideColors(dmsPath, homeDir string, state greeterThemeSyncState, logFunc func(string)) error {
|
||||
if !state.UsesDynamicWallpaperOverride {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, err := os.Stat(state.ResolvedGreeterWallpaperPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", state.ResolvedGreeterWallpaperPath, err)
|
||||
}
|
||||
if st.IsDir() {
|
||||
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", state.ResolvedGreeterWallpaperPath)
|
||||
}
|
||||
|
||||
mode := matugen.ColorModeDark
|
||||
if state.IsLightMode {
|
||||
mode = matugen.ColorModeLight
|
||||
}
|
||||
|
||||
opts := matugen.Options{
|
||||
StateDir: greeterOverrideColorsStateDir(homeDir),
|
||||
ShellDir: dmsPath,
|
||||
ConfigDir: filepath.Join(homeDir, ".config"),
|
||||
Kind: "image",
|
||||
Value: state.ResolvedGreeterWallpaperPath,
|
||||
Mode: mode,
|
||||
IconTheme: state.IconTheme,
|
||||
MatugenType: state.MatugenScheme,
|
||||
RunUserTemplates: false,
|
||||
ColorsOnly: true,
|
||||
}
|
||||
|
||||
err = matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
logFunc("✓ Greeter dynamic override colors already up to date")
|
||||
return nil
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to generate greeter dynamic colors from wallpaper override: %w", err)
|
||||
default:
|
||||
logFunc("✓ Generated greeter dynamic colors from wallpaper override")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncState, logFunc func(string), sudoPassword string) error {
|
||||
source := state.effectiveColorsSource(homeDir)
|
||||
if !state.UsesDynamicWallpaperOverride {
|
||||
if err := ensureGreeterSyncSourceFile(source); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if _, err := os.Stat(source); err != nil {
|
||||
return fmt.Errorf("expected generated greeter colors at %s: %w", source, err)
|
||||
}
|
||||
|
||||
target := filepath.Join(cacheDir, "colors.json")
|
||||
_ = runSudoCmd(sudoPassword, "rm", "-f", target)
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", source, target); err != nil {
|
||||
return fmt.Errorf("failed to create symlink for wallpaper based theming (%s -> %s): %w", target, source, err)
|
||||
}
|
||||
|
||||
if state.UsesDynamicWallpaperOverride {
|
||||
logFunc("✓ Synced wallpaper based theming (greeter wallpaper override)")
|
||||
} else {
|
||||
logFunc("✓ Synced wallpaper based theming")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
@@ -1132,11 +1284,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
target: filepath.Join(cacheDir, "session.json"),
|
||||
desc: "state (wallpaper configuration)",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
||||
target: filepath.Join(cacheDir, "colors.json"),
|
||||
desc: "wallpaper based theming",
|
||||
},
|
||||
}
|
||||
|
||||
for _, link := range symlinks {
|
||||
@@ -1162,12 +1309,21 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||
}
|
||||
|
||||
if err := syncGreeterWallpaperOverride(homeDir, cacheDir, logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
||||
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve greeter color source: %w", err)
|
||||
}
|
||||
|
||||
if err := syncGreeterPamConfig(homeDir, logFunc, sudoPassword, forceAuth); err != nil {
|
||||
return fmt.Errorf("greeter PAM config sync failed: %w", err)
|
||||
if err := syncGreeterDynamicOverrideColors(dmsPath, homeDir, state, logFunc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syncGreeterColorSource(homeDir, cacheDir, state, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syncGreeterWallpaperOverride(cacheDir, logFunc, sudoPassword, state); err != nil {
|
||||
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
||||
}
|
||||
|
||||
if strings.ToLower(compositor) != "niri" {
|
||||
@@ -1181,23 +1337,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string), sudoPassword string) error {
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
var settings struct {
|
||||
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) error {
|
||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||
if settings.GreeterWallpaperPath == "" {
|
||||
if state.ResolvedGreeterWallpaperPath == "" {
|
||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||
}
|
||||
@@ -1207,10 +1349,7 @@ func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string)
|
||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||
}
|
||||
src := settings.GreeterWallpaperPath
|
||||
if !filepath.IsAbs(src) {
|
||||
src = filepath.Join(homeDir, src)
|
||||
}
|
||||
src := state.ResolvedGreeterWallpaperPath
|
||||
st, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
|
||||
@@ -1235,378 +1374,6 @@ func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pamModuleExists(module string) bool {
|
||||
for _, libDir := range []string{
|
||||
"/usr/lib64/security",
|
||||
"/usr/lib/security",
|
||||
"/lib64/security",
|
||||
"/lib/security",
|
||||
"/lib/x86_64-linux-gnu/security",
|
||||
"/usr/lib/x86_64-linux-gnu/security",
|
||||
"/lib/aarch64-linux-gnu/security",
|
||||
"/usr/lib/aarch64-linux-gnu/security",
|
||||
"/run/current-system/sw/lib64/security",
|
||||
"/run/current-system/sw/lib/security",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
filtered := make([]string, 0, len(lines))
|
||||
inManagedBlock := false
|
||||
removed := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == GreeterPamManagedBlockStart {
|
||||
inManagedBlock = true
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
if trimmed == GreeterPamManagedBlockEnd {
|
||||
inManagedBlock = false
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
if inManagedBlock {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, line)
|
||||
}
|
||||
|
||||
return strings.Join(filtered, "\n"), removed
|
||||
}
|
||||
|
||||
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
filtered := make([]string, 0, len(lines))
|
||||
removed := false
|
||||
|
||||
for i := 0; i < len(lines); i++ {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
||||
removed = true
|
||||
if i+1 < len(lines) {
|
||||
nextLine := strings.TrimSpace(lines[i+1])
|
||||
if strings.HasPrefix(nextLine, "auth") &&
|
||||
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, lines[i])
|
||||
}
|
||||
|
||||
return strings.Join(filtered, "\n"), removed
|
||||
}
|
||||
|
||||
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
||||
block := strings.Join(blockLines, "\n")
|
||||
prefix := strings.Join(lines[:i], "\n")
|
||||
suffix := strings.Join(lines[i:], "\n")
|
||||
switch {
|
||||
case prefix == "":
|
||||
return block + "\n" + suffix, nil
|
||||
case suffix == "":
|
||||
return prefix + "\n" + block, nil
|
||||
default:
|
||||
return prefix + "\n" + block + "\n" + suffix, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
||||
}
|
||||
|
||||
func PamTextIncludesFile(pamText, filename string) bool {
|
||||
lines := strings.Split(pamText, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, filename) &&
|
||||
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PamFileHasModule(pamFilePath, module string) bool {
|
||||
data, err := os.ReadFile(pamFilePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, module) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DetectIncludedPamModule(pamText, module string) string {
|
||||
for _, includedFile := range includedPamAuthFiles {
|
||||
if PamTextIncludesFile(pamText, includedFile) && PamFileHasModule("/etc/pam.d/"+includedFile, module) {
|
||||
return includedFile
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type greeterAuthSettings struct {
|
||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||
}
|
||||
|
||||
func readGreeterAuthSettings(homeDir string) (greeterAuthSettings, error) {
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return greeterAuthSettings{}, nil
|
||||
}
|
||||
return greeterAuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == "" {
|
||||
return greeterAuthSettings{}, nil
|
||||
}
|
||||
var settings greeterAuthSettings
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return greeterAuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
||||
settings, err := readGreeterAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
||||
}
|
||||
|
||||
func hasEnrolledFingerprintOutput(output string) bool {
|
||||
lower := strings.ToLower(output)
|
||||
if strings.Contains(lower, "no fingers enrolled") ||
|
||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
||||
strings.Contains(lower, "no prints enrolled") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lower, "has fingers enrolled") ||
|
||||
strings.Contains(lower, "has fingerprints enrolled") {
|
||||
return true
|
||||
}
|
||||
for _, line := range strings.Split(lower, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "finger:") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FingerprintAuthAvailableForUser(username string) bool {
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
if !pamModuleExists("pam_fprintd.so") {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasEnrolledFingerprintOutput(string(out))
|
||||
}
|
||||
|
||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
||||
if username == "" {
|
||||
username = strings.TrimSpace(os.Getenv("USER"))
|
||||
}
|
||||
if username == "" {
|
||||
out, err := exec.Command("id", "-un").Output()
|
||||
if err == nil {
|
||||
username = strings.TrimSpace(string(out))
|
||||
}
|
||||
}
|
||||
return FingerprintAuthAvailableForUser(username)
|
||||
}
|
||||
|
||||
func pamManagerHintForCurrentDistro() string {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyFedora:
|
||||
return "Disable it in authselect to force password-only greeter login."
|
||||
case distros.FamilyDebian, distros.FamilyUbuntu:
|
||||
return "Disable it in pam-auth-update to force password-only greeter login."
|
||||
default:
|
||||
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
}
|
||||
|
||||
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
||||
var wantFprint, wantU2f bool
|
||||
fprintToggleEnabled := forceAuth
|
||||
u2fToggleEnabled := forceAuth
|
||||
if forceAuth {
|
||||
wantFprint = pamModuleExists("pam_fprintd.so")
|
||||
wantU2f = pamModuleExists("pam_u2f.so")
|
||||
} else {
|
||||
settings, err := readGreeterAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
||||
u2fToggleEnabled = settings.GreeterEnableU2f
|
||||
fprintModule := pamModuleExists("pam_fprintd.so")
|
||||
u2fModule := pamModuleExists("pam_u2f.so")
|
||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
||||
wantU2f = settings.GreeterEnableU2f && u2fModule
|
||||
if settings.GreeterEnableFprint && !fprintModule {
|
||||
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
||||
}
|
||||
if settings.GreeterEnableU2f && !u2fModule {
|
||||
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
if IsNixOS() {
|
||||
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
||||
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
||||
return nil
|
||||
}
|
||||
|
||||
greetdPamPath := "/etc/pam.d/greetd"
|
||||
pamData, err := os.ReadFile(greetdPamPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
|
||||
}
|
||||
originalContent := string(pamData)
|
||||
content, _ := stripManagedGreeterPamBlock(originalContent)
|
||||
content, _ = stripLegacyGreeterPamLines(content)
|
||||
|
||||
includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so")
|
||||
includedU2fFile := DetectIncludedPamModule(content, "pam_u2f.so")
|
||||
fprintAvailableForCurrentUser := FingerprintAuthAvailableForCurrentUser()
|
||||
if wantFprint && includedFprintFile != "" {
|
||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
||||
wantFprint = false
|
||||
}
|
||||
if wantU2f && includedU2fFile != "" {
|
||||
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
||||
wantU2f = false
|
||||
}
|
||||
if !wantFprint && includedFprintFile != "" {
|
||||
if fprintToggleEnabled {
|
||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||
if fprintAvailableForCurrentUser {
|
||||
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
||||
} else {
|
||||
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
||||
}
|
||||
} else {
|
||||
if fprintAvailableForCurrentUser {
|
||||
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
||||
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
||||
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
||||
} else {
|
||||
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
||||
logFunc(" Password auth remains the effective login path.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !wantU2f && includedU2fFile != "" {
|
||||
if u2fToggleEnabled {
|
||||
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
||||
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
||||
} else {
|
||||
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
||||
logFunc(" " + pamManagerHintForCurrentDistro())
|
||||
}
|
||||
}
|
||||
|
||||
if wantFprint || wantU2f {
|
||||
blockLines := []string{GreeterPamManagedBlockStart}
|
||||
if wantFprint {
|
||||
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
|
||||
}
|
||||
if wantU2f {
|
||||
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
||||
}
|
||||
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
||||
|
||||
content, err = insertManagedGreeterPamBlock(content, blockLines, greetdPamPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if content == originalContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "greetd-pam-*.conf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := tmpFile.WriteString(content); err != nil {
|
||||
tmpFile.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
|
||||
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
|
||||
}
|
||||
if wantFprint || wantU2f {
|
||||
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
||||
} else {
|
||||
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type niriGreeterSync struct {
|
||||
processed map[string]bool
|
||||
nodes []*document.Node
|
||||
@@ -2280,10 +2047,15 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
|
||||
}
|
||||
|
||||
logFunc("Synchronizing DMS configurations...")
|
||||
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword, false); err != nil {
|
||||
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
||||
}
|
||||
|
||||
logFunc("Configuring authentication...")
|
||||
if err := sharedpam.SyncAuthConfig(logFunc, sudoPassword, sharedpam.SyncAuthOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to sync authentication: %w", err)
|
||||
}
|
||||
|
||||
logFunc("Checking for conflicting display managers...")
|
||||
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
||||
|
||||
98
core/internal/greeter/installer_test.go
Normal file
98
core/internal/greeter/installer_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package greeter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTestFile(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()
|
||||
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||
writeTestFile(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,9 @@ type Options struct {
|
||||
Mode ColorMode
|
||||
IconTheme string
|
||||
MatugenType string
|
||||
Contrast float64
|
||||
RunUserTemplates bool
|
||||
ColorsOnly bool
|
||||
StockColors string
|
||||
SyncModeWithPortal bool
|
||||
TerminalsAlwaysDark bool
|
||||
@@ -227,6 +229,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
log.Info("Running matugen color hex with stock color overrides")
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -263,6 +266,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
args = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = appendContrastArg(args, opts.Contrast)
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return false, err
|
||||
@@ -274,6 +278,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if opts.ColorsOnly {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if isDMSGTKActive(opts.ConfigDir) {
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
@@ -294,6 +302,13 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func appendContrastArg(args []string, contrast float64) []string {
|
||||
if contrast == 0 {
|
||||
return args
|
||||
}
|
||||
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||
|
||||
@@ -331,6 +346,10 @@ output_path = '%s'
|
||||
|
||||
`, opts.ShellDir, opts.ColorsOutput())
|
||||
|
||||
if opts.ColorsOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
for _, tmpl := range templateRegistry {
|
||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||
@@ -597,10 +616,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||
matugenVersionOK = true
|
||||
|
||||
if matugenSupportsCOE {
|
||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -678,6 +697,7 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||
baseArgs = []string{opts.Kind, opts.Value}
|
||||
}
|
||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||
if flags.isV4 {
|
||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package matugen
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
892
core/internal/pam/pam.go
Normal file
892
core/internal/pam/pam.go
Normal file
@@ -0,0 +1,892 @@
|
||||
package pam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
)
|
||||
|
||||
const (
|
||||
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
|
||||
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
|
||||
|
||||
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
|
||||
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
|
||||
|
||||
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
|
||||
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
|
||||
|
||||
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
|
||||
legacyGreeterPamU2FComment = "# DMS greeter U2F"
|
||||
|
||||
GreetdPamPath = "/etc/pam.d/greetd"
|
||||
DankshellPamPath = "/etc/pam.d/dankshell"
|
||||
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
|
||||
)
|
||||
|
||||
var includedPamAuthFiles = []string{
|
||||
"system-auth",
|
||||
"common-auth",
|
||||
"password-auth",
|
||||
"system-login",
|
||||
"system-local-login",
|
||||
"common-auth-pc",
|
||||
"login",
|
||||
}
|
||||
|
||||
type AuthSettings struct {
|
||||
EnableFprint bool `json:"enableFprint"`
|
||||
EnableU2f bool `json:"enableU2f"`
|
||||
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||
}
|
||||
|
||||
type SyncAuthOptions struct {
|
||||
HomeDir string
|
||||
ForceGreeterAuth bool
|
||||
}
|
||||
|
||||
type syncDeps struct {
|
||||
pamDir string
|
||||
greetdPath string
|
||||
dankshellPath string
|
||||
dankshellU2fPath string
|
||||
isNixOS func() bool
|
||||
readFile func(string) ([]byte, error)
|
||||
stat func(string) (os.FileInfo, error)
|
||||
createTemp func(string, string) (*os.File, error)
|
||||
removeFile func(string) error
|
||||
runSudoCmd func(string, string, ...string) error
|
||||
pamModuleExists func(string) bool
|
||||
fingerprintAvailableForCurrentUser func() bool
|
||||
}
|
||||
|
||||
type lockscreenPamIncludeDirective struct {
|
||||
target string
|
||||
filterType string
|
||||
}
|
||||
|
||||
type lockscreenPamResolver struct {
|
||||
pamDir string
|
||||
readFile func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
func defaultSyncDeps() syncDeps {
|
||||
return syncDeps{
|
||||
pamDir: "/etc/pam.d",
|
||||
greetdPath: GreetdPamPath,
|
||||
dankshellPath: DankshellPamPath,
|
||||
dankshellU2fPath: DankshellU2FPamPath,
|
||||
isNixOS: IsNixOS,
|
||||
readFile: os.ReadFile,
|
||||
stat: os.Stat,
|
||||
createTemp: os.CreateTemp,
|
||||
removeFile: os.Remove,
|
||||
runSudoCmd: runSudoCmd,
|
||||
pamModuleExists: pamModuleExists,
|
||||
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
|
||||
}
|
||||
}
|
||||
|
||||
func IsNixOS() bool {
|
||||
_, err := os.Stat("/etc/NIXOS")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func ReadAuthSettings(homeDir string) (AuthSettings, error) {
|
||||
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return AuthSettings{}, nil
|
||||
}
|
||||
return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == "" {
|
||||
return AuthSettings{}, nil
|
||||
}
|
||||
|
||||
var settings AuthSettings
|
||||
if err := json.Unmarshal(data, &settings); err != nil {
|
||||
return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
|
||||
settings, err := ReadAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
|
||||
}
|
||||
|
||||
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
|
||||
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
|
||||
}
|
||||
|
||||
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
|
||||
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
|
||||
}
|
||||
|
||||
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
|
||||
homeDir := strings.TrimSpace(options.HomeDir)
|
||||
if homeDir == "" {
|
||||
var err error
|
||||
homeDir, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := ReadAuthSettings(homeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := deps.stat(deps.greetdPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
|
||||
}
|
||||
|
||||
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
||||
if deps.isNixOS() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := deps.readFile(deps.greetdPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
||||
}
|
||||
|
||||
originalContent := string(data)
|
||||
stripped, removed := stripManagedGreeterPamBlock(originalContent)
|
||||
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
||||
if !removed && !removedLegacy {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
|
||||
}
|
||||
|
||||
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||
if pamText == "" {
|
||||
return false, false, false, false
|
||||
}
|
||||
|
||||
lines := strings.Split(pamText, "\n")
|
||||
inManaged := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch trimmed {
|
||||
case GreeterPamManagedBlockStart:
|
||||
managed = true
|
||||
inManaged = true
|
||||
continue
|
||||
case GreeterPamManagedBlockEnd:
|
||||
inManaged = false
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
||||
legacy = true
|
||||
}
|
||||
if !inManaged {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_fprintd") {
|
||||
fingerprint = true
|
||||
}
|
||||
if strings.Contains(trimmed, "pam_u2f") {
|
||||
u2f = true
|
||||
}
|
||||
}
|
||||
|
||||
return managed, fingerprint, u2f, legacy
|
||||
}
|
||||
|
||||
func StripManagedGreeterPamContent(pamText string) (string, bool) {
|
||||
stripped, removed := stripManagedGreeterPamBlock(pamText)
|
||||
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
|
||||
return stripped, removed || removedLegacy
|
||||
}
|
||||
|
||||
func PamTextIncludesFile(pamText, filename string) bool {
|
||||
lines := strings.Split(pamText, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, filename) &&
|
||||
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PamFileHasModule(pamFilePath, module string) bool {
|
||||
data, err := os.ReadFile(pamFilePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return pamContentHasModule(string(data), module)
|
||||
}
|
||||
|
||||
func DetectIncludedPamModule(pamText, module string) string {
|
||||
return detectIncludedPamModule(pamText, module, defaultSyncDeps())
|
||||
}
|
||||
|
||||
func detectIncludedPamModule(pamText, module string, deps syncDeps) string {
|
||||
for _, includedFile := range includedPamAuthFiles {
|
||||
if !PamTextIncludesFile(pamText, includedFile) {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(deps.pamDir, includedFile)
|
||||
data, err := deps.readFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if pamContentHasModule(string(data), module) {
|
||||
return includedFile
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pamContentHasModule(content, module string) bool {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, module) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasManagedLockscreenPamFile(content string) bool {
|
||||
return strings.Contains(content, LockscreenPamManagedBlockStart) &&
|
||||
strings.Contains(content, LockscreenPamManagedBlockEnd)
|
||||
}
|
||||
|
||||
func hasManagedLockscreenU2FPamFile(content string) bool {
|
||||
return strings.Contains(content, LockscreenU2FPamManagedBlockStart) &&
|
||||
strings.Contains(content, LockscreenU2FPamManagedBlockEnd)
|
||||
}
|
||||
|
||||
func pamDirectiveType(line string) string {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
directiveType := strings.TrimPrefix(fields[0], "-")
|
||||
switch directiveType {
|
||||
case "auth", "account", "password", "session":
|
||||
return directiveType
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func isExcludedLockscreenPamLine(line string) bool {
|
||||
for _, field := range strings.Fields(line) {
|
||||
if strings.HasPrefix(field, "#") {
|
||||
break
|
||||
}
|
||||
if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) {
|
||||
fields := strings.Fields(trimmed)
|
||||
if len(fields) >= 2 && fields[0] == "@include" {
|
||||
return lockscreenPamIncludeDirective{
|
||||
target: fields[1],
|
||||
filterType: inheritedFilter,
|
||||
}, true
|
||||
}
|
||||
|
||||
if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") {
|
||||
lineType := pamDirectiveType(trimmed)
|
||||
if lineType == "" {
|
||||
return lockscreenPamIncludeDirective{}, false
|
||||
}
|
||||
return lockscreenPamIncludeDirective{
|
||||
target: fields[2],
|
||||
filterType: lineType,
|
||||
}, true
|
||||
}
|
||||
|
||||
if len(fields) >= 3 && fields[1] == "@include" {
|
||||
lineType := pamDirectiveType(trimmed)
|
||||
if lineType == "" {
|
||||
return lockscreenPamIncludeDirective{}, false
|
||||
}
|
||||
return lockscreenPamIncludeDirective{
|
||||
target: fields[2],
|
||||
filterType: lineType,
|
||||
}, true
|
||||
}
|
||||
|
||||
return lockscreenPamIncludeDirective{}, false
|
||||
}
|
||||
|
||||
func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
|
||||
if strings.TrimSpace(target) == "" {
|
||||
return "", fmt.Errorf("empty PAM include target")
|
||||
}
|
||||
|
||||
cleanPamDir := filepath.Clean(pamDir)
|
||||
if filepath.IsAbs(target) {
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if filepath.Dir(cleanTarget) != cleanPamDir {
|
||||
return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
|
||||
}
|
||||
return cleanTarget, nil
|
||||
}
|
||||
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("invalid PAM include target: %s", target)
|
||||
}
|
||||
|
||||
return filepath.Join(cleanPamDir, cleanTarget), nil
|
||||
}
|
||||
|
||||
func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
|
||||
path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, seen := range stack {
|
||||
if seen == path {
|
||||
chain := append(append([]string{}, stack...), path)
|
||||
display := make([]string, 0, len(chain))
|
||||
for _, item := range chain {
|
||||
display = append(display, filepath.Base(item))
|
||||
}
|
||||
return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
|
||||
}
|
||||
}
|
||||
|
||||
data, err := r.readFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err)
|
||||
}
|
||||
|
||||
var resolved []string
|
||||
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
|
||||
rawLine = strings.TrimRight(rawLine, "\r")
|
||||
trimmed := strings.TrimSpace(rawLine)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" {
|
||||
continue
|
||||
}
|
||||
|
||||
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
|
||||
lineType := pamDirectiveType(trimmed)
|
||||
if filterType != "" && lineType != "" && lineType != filterType {
|
||||
continue
|
||||
}
|
||||
|
||||
nested, err := r.resolveService(include.target, include.filterType, append(stack, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, nested...)
|
||||
continue
|
||||
}
|
||||
|
||||
lineType := pamDirectiveType(trimmed)
|
||||
if lineType == "" {
|
||||
return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed)
|
||||
}
|
||||
if filterType != "" && lineType != filterType {
|
||||
continue
|
||||
}
|
||||
if isExcludedLockscreenPamLine(trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved = append(resolved, rawLine)
|
||||
}
|
||||
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
|
||||
resolver := lockscreenPamResolver{
|
||||
pamDir: pamDir,
|
||||
readFile: readFile,
|
||||
}
|
||||
|
||||
resolvedLines, err := resolver.resolveService("login", "", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(resolvedLines) == 0 {
|
||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||
}
|
||||
|
||||
hasAuth := false
|
||||
for _, line := range resolvedLines {
|
||||
if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
|
||||
hasAuth = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAuth {
|
||||
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("#%PAM-1.0\n")
|
||||
b.WriteString(LockscreenPamManagedBlockStart + "\n")
|
||||
for _, line := range resolvedLines {
|
||||
b.WriteString(line)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString(LockscreenPamManagedBlockEnd + "\n")
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func buildManagedLockscreenU2FPamContent() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("#%PAM-1.0\n")
|
||||
b.WriteString(LockscreenU2FPamManagedBlockStart + "\n")
|
||||
b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n")
|
||||
b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
|
||||
if deps.isNixOS() {
|
||||
logFunc("ℹ NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
|
||||
return nil
|
||||
}
|
||||
|
||||
existingData, err := deps.readFile(deps.dankshellPath)
|
||||
if err == nil {
|
||||
if !hasManagedLockscreenPamFile(string(existingData)) {
|
||||
logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.")
|
||||
return nil
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
|
||||
}
|
||||
|
||||
content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
|
||||
}
|
||||
|
||||
if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err)
|
||||
}
|
||||
|
||||
logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error {
|
||||
if deps.isNixOS() {
|
||||
logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.")
|
||||
return nil
|
||||
}
|
||||
|
||||
existingData, err := deps.readFile(deps.dankshellU2fPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err)
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
||||
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.")
|
||||
return nil
|
||||
}
|
||||
if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err)
|
||||
}
|
||||
logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
|
||||
logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil {
|
||||
return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err)
|
||||
}
|
||||
logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f")
|
||||
return nil
|
||||
}
|
||||
|
||||
func stripManagedGreeterPamBlock(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
filtered := make([]string, 0, len(lines))
|
||||
inManagedBlock := false
|
||||
removed := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == GreeterPamManagedBlockStart {
|
||||
inManagedBlock = true
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
if trimmed == GreeterPamManagedBlockEnd {
|
||||
inManagedBlock = false
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
if inManagedBlock {
|
||||
removed = true
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, line)
|
||||
}
|
||||
|
||||
return strings.Join(filtered, "\n"), removed
|
||||
}
|
||||
|
||||
func stripLegacyGreeterPamLines(content string) (string, bool) {
|
||||
lines := strings.Split(content, "\n")
|
||||
filtered := make([]string, 0, len(lines))
|
||||
removed := false
|
||||
|
||||
for i := 0; i < len(lines); i++ {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
|
||||
removed = true
|
||||
if i+1 < len(lines) {
|
||||
nextLine := strings.TrimSpace(lines[i+1])
|
||||
if strings.HasPrefix(nextLine, "auth") &&
|
||||
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, lines[i])
|
||||
}
|
||||
|
||||
return strings.Join(filtered, "\n"), removed
|
||||
}
|
||||
|
||||
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
|
||||
block := strings.Join(blockLines, "\n")
|
||||
prefix := strings.Join(lines[:i], "\n")
|
||||
suffix := strings.Join(lines[i:], "\n")
|
||||
switch {
|
||||
case prefix == "":
|
||||
return block + "\n" + suffix, nil
|
||||
case suffix == "":
|
||||
return prefix + "\n" + block, nil
|
||||
default:
|
||||
return prefix + "\n" + block + "\n" + suffix, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
|
||||
}
|
||||
|
||||
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
|
||||
var wantFprint, wantU2f bool
|
||||
fprintToggleEnabled := forceAuth
|
||||
u2fToggleEnabled := forceAuth
|
||||
if forceAuth {
|
||||
wantFprint = deps.pamModuleExists("pam_fprintd.so")
|
||||
wantU2f = deps.pamModuleExists("pam_u2f.so")
|
||||
} else {
|
||||
fprintToggleEnabled = settings.GreeterEnableFprint
|
||||
u2fToggleEnabled = settings.GreeterEnableU2f
|
||||
fprintModule := deps.pamModuleExists("pam_fprintd.so")
|
||||
u2fModule := deps.pamModuleExists("pam_u2f.so")
|
||||
wantFprint = settings.GreeterEnableFprint && fprintModule
|
||||
wantU2f = settings.GreeterEnableU2f && u2fModule
|
||||
if settings.GreeterEnableFprint && !fprintModule {
|
||||
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
|
||||
}
|
||||
if settings.GreeterEnableU2f && !u2fModule {
|
||||
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
|
||||
}
|
||||
}
|
||||
|
||||
if deps.isNixOS() {
|
||||
logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
|
||||
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
|
||||
return nil
|
||||
}
|
||||
|
||||
pamData, err := deps.readFile(deps.greetdPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
|
||||
}
|
||||
originalContent := string(pamData)
|
||||
content, _ := stripManagedGreeterPamBlock(originalContent)
|
||||
content, _ = stripLegacyGreeterPamLines(content)
|
||||
|
||||
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
|
||||
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
|
||||
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
|
||||
if wantFprint && includedFprintFile != "" {
|
||||
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
|
||||
wantFprint = false
|
||||
}
|
||||
if wantU2f && includedU2fFile != "" {
|
||||
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
|
||||
wantU2f = false
|
||||
}
|
||||
if !wantFprint && includedFprintFile != "" {
|
||||
if fprintToggleEnabled {
|
||||
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||
if fprintAvailableForCurrentUser {
|
||||
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
|
||||
} else {
|
||||
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
|
||||
}
|
||||
} else {
|
||||
if fprintAvailableForCurrentUser {
|
||||
logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
|
||||
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
|
||||
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
|
||||
} else {
|
||||
logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
|
||||
logFunc(" Password auth remains the effective login path.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !wantU2f && includedU2fFile != "" {
|
||||
if u2fToggleEnabled {
|
||||
logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".")
|
||||
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
|
||||
} else {
|
||||
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
|
||||
logFunc(" " + pamManagerHintForCurrentDistro())
|
||||
}
|
||||
}
|
||||
|
||||
if wantFprint || wantU2f {
|
||||
blockLines := []string{GreeterPamManagedBlockStart}
|
||||
if wantFprint {
|
||||
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
|
||||
}
|
||||
if wantU2f {
|
||||
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
|
||||
}
|
||||
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
|
||||
|
||||
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if content == originalContent {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
|
||||
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
|
||||
}
|
||||
if wantFprint || wantU2f {
|
||||
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
|
||||
} else {
|
||||
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
|
||||
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
_ = deps.removeFile(tmpPath)
|
||||
}()
|
||||
|
||||
if _, err := tmpFile.WriteString(content); err != nil {
|
||||
tmpFile.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||
return fmt.Errorf("failed to set permissions on %s: %w", destPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pamManagerHintForCurrentDistro() string {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyFedora:
|
||||
return "Disable it in authselect to force password-only greeter login."
|
||||
case distros.FamilyDebian, distros.FamilyUbuntu:
|
||||
return "Disable it in pam-auth-update to force password-only greeter login."
|
||||
default:
|
||||
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
|
||||
}
|
||||
}
|
||||
|
||||
func pamModuleExists(module string) bool {
|
||||
for _, libDir := range []string{
|
||||
"/usr/lib64/security",
|
||||
"/usr/lib/security",
|
||||
"/lib64/security",
|
||||
"/lib/security",
|
||||
"/lib/x86_64-linux-gnu/security",
|
||||
"/usr/lib/x86_64-linux-gnu/security",
|
||||
"/lib/aarch64-linux-gnu/security",
|
||||
"/usr/lib/aarch64-linux-gnu/security",
|
||||
"/run/current-system/sw/lib64/security",
|
||||
"/run/current-system/sw/lib/security",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasEnrolledFingerprintOutput(output string) bool {
|
||||
lower := strings.ToLower(output)
|
||||
if strings.Contains(lower, "no fingers enrolled") ||
|
||||
strings.Contains(lower, "no fingerprints enrolled") ||
|
||||
strings.Contains(lower, "no prints enrolled") {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(lower, "has fingers enrolled") ||
|
||||
strings.Contains(lower, "has fingerprints enrolled") {
|
||||
return true
|
||||
}
|
||||
for _, line := range strings.Split(lower, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "finger:") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FingerprintAuthAvailableForCurrentUser() bool {
|
||||
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
|
||||
if username == "" {
|
||||
username = strings.TrimSpace(os.Getenv("USER"))
|
||||
}
|
||||
if username == "" {
|
||||
out, err := exec.Command("id", "-un").Output()
|
||||
if err == nil {
|
||||
username = strings.TrimSpace(string(out))
|
||||
}
|
||||
}
|
||||
return fingerprintAuthAvailableForUser(username)
|
||||
}
|
||||
|
||||
func fingerprintAuthAvailableForUser(username string) bool {
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return false
|
||||
}
|
||||
if !pamModuleExists("pam_fprintd.so") {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("fprintd-list"); err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasEnrolledFingerprintOutput(string(out))
|
||||
}
|
||||
|
||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if sudoPassword != "" {
|
||||
fullArgs := append([]string{command}, args...)
|
||||
quotedArgs := make([]string, len(fullArgs))
|
||||
for i, arg := range fullArgs {
|
||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
||||
}
|
||||
cmdStr := strings.Join(quotedArgs, " ")
|
||||
|
||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
||||
} else {
|
||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
671
core/internal/pam/pam_test.go
Normal file
671
core/internal/pam/pam_test.go
Normal file
@@ -0,0 +1,671 @@
|
||||
package pam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeTestFile(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)
|
||||
}
|
||||
}
|
||||
|
||||
type pamTestEnv struct {
|
||||
pamDir string
|
||||
greetdPath string
|
||||
dankshellPath string
|
||||
dankshellU2fPath string
|
||||
tmpDir string
|
||||
homeDir string
|
||||
availableModules map[string]bool
|
||||
fingerprintAvailable bool
|
||||
}
|
||||
|
||||
func newPamTestEnv(t *testing.T) *pamTestEnv {
|
||||
t.Helper()
|
||||
|
||||
root := t.TempDir()
|
||||
pamDir := filepath.Join(root, "pam.d")
|
||||
tmpDir := filepath.Join(root, "tmp")
|
||||
homeDir := filepath.Join(root, "home")
|
||||
|
||||
for _, dir := range []string{pamDir, tmpDir, homeDir} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &pamTestEnv{
|
||||
pamDir: pamDir,
|
||||
greetdPath: filepath.Join(pamDir, "greetd"),
|
||||
dankshellPath: filepath.Join(pamDir, "dankshell"),
|
||||
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
|
||||
tmpDir: tmpDir,
|
||||
homeDir: homeDir,
|
||||
availableModules: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
|
||||
t.Helper()
|
||||
writeTestFile(t, filepath.Join(e.pamDir, name), content)
|
||||
}
|
||||
|
||||
func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
|
||||
t.Helper()
|
||||
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
|
||||
}
|
||||
|
||||
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
|
||||
return syncDeps{
|
||||
pamDir: e.pamDir,
|
||||
greetdPath: e.greetdPath,
|
||||
dankshellPath: e.dankshellPath,
|
||||
dankshellU2fPath: e.dankshellU2fPath,
|
||||
isNixOS: func() bool { return isNixOS },
|
||||
readFile: os.ReadFile,
|
||||
stat: os.Stat,
|
||||
createTemp: func(_ string, pattern string) (*os.File, error) {
|
||||
return os.CreateTemp(e.tmpDir, pattern)
|
||||
},
|
||||
removeFile: os.Remove,
|
||||
runSudoCmd: func(_ string, command string, args ...string) error {
|
||||
switch command {
|
||||
case "cp":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("unexpected cp args: %v", args)
|
||||
}
|
||||
data, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(args[1], data, 0o644)
|
||||
case "chmod":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("unexpected chmod args: %v", args)
|
||||
}
|
||||
return nil
|
||||
case "rm":
|
||||
if len(args) != 2 || args[0] != "-f" {
|
||||
return fmt.Errorf("unexpected rm args: %v", args)
|
||||
}
|
||||
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
|
||||
}
|
||||
},
|
||||
pamModuleExists: func(module string) bool {
|
||||
return e.availableModules[module]
|
||||
},
|
||||
fingerprintAvailableForCurrentUser: func() bool {
|
||||
return e.fingerprintAvailable
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func readFileString(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read %s: %v", path, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestHasManagedLockscreenPamFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "both markers present",
|
||||
content: "#%PAM-1.0\n" +
|
||||
LockscreenPamManagedBlockStart + "\n" +
|
||||
"auth sufficient pam_unix.so\n" +
|
||||
LockscreenPamManagedBlockEnd + "\n",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "missing end marker is not managed",
|
||||
content: "#%PAM-1.0\n" +
|
||||
LockscreenPamManagedBlockStart + "\n" +
|
||||
"auth sufficient pam_unix.so\n",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "custom file is not managed",
|
||||
content: "#%PAM-1.0\nauth sufficient pam_unix.so\n",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := hasManagedLockscreenPamFile(tt.content); got != tt.want {
|
||||
t.Fatalf("hasManagedLockscreenPamFile() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildManagedLockscreenPamContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
wantCounts map[string]int
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "preserves custom modules and strips direct u2f and fprint directives",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\n" +
|
||||
"auth include system-auth\n" +
|
||||
"account include system-auth\n" +
|
||||
"session include system-auth\n",
|
||||
"system-auth": "auth requisite pam_nologin.so\n" +
|
||||
"auth sufficient pam_unix.so try_first_pass nullok\n" +
|
||||
"auth sufficient pam_u2f.so cue\n" +
|
||||
"auth sufficient pam_fprintd.so max-tries=1\n" +
|
||||
"auth required pam_radius_auth.so conf=/etc/raddb/server\n" +
|
||||
"account required pam_access.so\n" +
|
||||
"session optional pam_lastlog.so silent\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"#%PAM-1.0",
|
||||
LockscreenPamManagedBlockStart,
|
||||
LockscreenPamManagedBlockEnd,
|
||||
"auth requisite pam_nologin.so",
|
||||
"auth sufficient pam_unix.so try_first_pass nullok",
|
||||
"auth required pam_radius_auth.so conf=/etc/raddb/server",
|
||||
"account required pam_access.so",
|
||||
"session optional pam_lastlog.so silent",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"pam_u2f",
|
||||
"pam_fprintd",
|
||||
},
|
||||
wantCounts: map[string]int{
|
||||
"auth required pam_radius_auth.so conf=/etc/raddb/server": 1,
|
||||
"account required pam_access.so": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "resolves nested include substack and @include transitively",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\n" +
|
||||
"auth include system-auth\n" +
|
||||
"account include system-auth\n" +
|
||||
"password include system-auth\n" +
|
||||
"session include system-auth\n",
|
||||
"system-auth": "auth substack custom-auth\n" +
|
||||
"account include custom-auth\n" +
|
||||
"password include custom-auth\n" +
|
||||
"session @include common-session\n",
|
||||
"custom-auth": "auth required pam_custom.so one=two\n" +
|
||||
"account required pam_custom_account.so\n" +
|
||||
"password required pam_custom_password.so\n",
|
||||
"common-session": "session optional pam_fprintd.so max-tries=1\n" +
|
||||
"session optional pam_lastlog.so silent\n",
|
||||
},
|
||||
wantContains: []string{
|
||||
"auth required pam_custom.so one=two",
|
||||
"account required pam_custom_account.so",
|
||||
"password required pam_custom_password.so",
|
||||
"session optional pam_lastlog.so silent",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"pam_fprintd",
|
||||
},
|
||||
wantCounts: map[string]int{
|
||||
"auth required pam_custom.so one=two": 1,
|
||||
"account required pam_custom_account.so": 1,
|
||||
"password required pam_custom_password.so": 1,
|
||||
"session optional pam_lastlog.so silent": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing include fails",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\nauth include missing-auth\n",
|
||||
},
|
||||
wantErr: "failed to read PAM file",
|
||||
},
|
||||
{
|
||||
name: "cyclic include fails",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\nauth include system-auth\n",
|
||||
"system-auth": "auth include login\n",
|
||||
},
|
||||
wantErr: "cyclic PAM include detected",
|
||||
},
|
||||
{
|
||||
name: "no auth directives remain after filtering fails",
|
||||
files: map[string]string{
|
||||
"login": "#%PAM-1.0\nauth include system-auth\n",
|
||||
"system-auth": "auth sufficient pam_u2f.so cue\n",
|
||||
},
|
||||
wantErr: "no auth directives remained after filtering",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
for name, content := range tt.files {
|
||||
env.writePamFile(t, name, content)
|
||||
}
|
||||
|
||||
content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile)
|
||||
if tt.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
|
||||
}
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Errorf("missing expected string %q in output:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
if strings.Contains(content, notWant) {
|
||||
t.Errorf("unexpected string %q found in output:\n%s", notWant, content)
|
||||
}
|
||||
}
|
||||
for want, wantCount := range tt.wantCounts {
|
||||
if gotCount := strings.Count(content, want); gotCount != wantCount {
|
||||
t.Errorf("count for %q = %d, want %d\noutput:\n%s", want, gotCount, wantCount, content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("custom dankshell file is skipped untouched", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
customContent := "#%PAM-1.0\nauth required pam_unix.so\n"
|
||||
env.writePamFile(t, "dankshell", customContent)
|
||||
|
||||
var logs []string
|
||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
if got := readFileString(t, env.dankshellPath); got != customContent {
|
||||
t.Fatalf("custom dankshell content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell found") {
|
||||
t.Fatalf("expected custom-file skip log, got %v", logs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("managed dankshell file is rewritten from resolved login stack", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\nauth sufficient pam_u2f.so cue\naccount required pam_access.so\n")
|
||||
env.writePamFile(t, "dankshell", "#%PAM-1.0\n"+LockscreenPamManagedBlockStart+"\nauth required pam_env.so\n"+LockscreenPamManagedBlockEnd+"\n")
|
||||
|
||||
var logs []string
|
||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
output := readFileString(t, env.dankshellPath)
|
||||
for _, want := range []string{
|
||||
LockscreenPamManagedBlockStart,
|
||||
"auth sufficient pam_unix.so try_first_pass nullok",
|
||||
"account required pam_access.so",
|
||||
LockscreenPamManagedBlockEnd,
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Errorf("missing expected string %q in rewritten dankshell:\n%s", want, output)
|
||||
}
|
||||
}
|
||||
if strings.Contains(output, "pam_u2f") {
|
||||
t.Errorf("rewritten dankshell still contains pam_u2f:\n%s", output)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell") {
|
||||
t.Fatalf("expected success log, got %v", logs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mutable systems fail when login stack cannot be converted safely", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
err := syncLockscreenPamConfigWithDeps(func(string) {}, "", env.deps(false))
|
||||
if err == nil {
|
||||
t.Fatal("expected error when login PAM file is missing, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to build") {
|
||||
t.Fatalf("error = %q, want substring %q", err.Error(), "failed to build")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NixOS remains informational and does not write dankshell", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
var logs []string
|
||||
|
||||
err := syncLockscreenPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", env.deps(true))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") {
|
||||
t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs)
|
||||
}
|
||||
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("enabled creates managed file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
var logs []string
|
||||
|
||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", true, env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readFileString(t, env.dankshellU2fPath)
|
||||
if got != buildManagedLockscreenU2FPamContent() {
|
||||
t.Fatalf("unexpected managed dankshell-u2f content:\n%s", got)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell-u2f") {
|
||||
t.Fatalf("expected create log, got %v", logs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("enabled rewrites existing managed file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\n"+LockscreenU2FPamManagedBlockStart+"\nauth required pam_u2f.so old\n"+LockscreenU2FPamManagedBlockEnd+"\n")
|
||||
|
||||
if err := syncLockscreenU2FPamConfigWithDeps(func(string) {}, "", true, env.deps(false)); err != nil {
|
||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
||||
t.Fatalf("managed dankshell-u2f was not rewritten:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled removes DMS-managed file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
|
||||
|
||||
var logs []string
|
||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", false, env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected managed dankshell-u2f to be removed, stat err = %v", err)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Removed DMS-managed /etc/pam.d/dankshell-u2f") {
|
||||
t.Fatalf("expected removal log, got %v", logs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disabled preserves custom file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
customContent := "#%PAM-1.0\nauth required pam_u2f.so cue\n"
|
||||
env.writePamFile(t, "dankshell-u2f", customContent)
|
||||
|
||||
var logs []string
|
||||
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", false, env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
if got := readFileString(t, env.dankshellU2fPath); got != customContent {
|
||||
t.Fatalf("custom dankshell-u2f content changed\ngot:\n%s\nwant:\n%s", got, customContent)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell-u2f found") {
|
||||
t.Fatalf("expected custom-file log, got %v", logs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.availableModules["pam_fprintd.so"] = true
|
||||
env.availableModules["pam_u2f.so"] = true
|
||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
|
||||
|
||||
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
|
||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readFileString(t, env.greetdPath)
|
||||
for _, want := range []string{
|
||||
GreeterPamManagedBlockStart,
|
||||
"auth sufficient pam_fprintd.so max-tries=1 timeout=5",
|
||||
"auth sufficient pam_u2f.so cue nouserok timeout=10",
|
||||
GreeterPamManagedBlockEnd,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
|
||||
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.availableModules["pam_fprintd.so"] = true
|
||||
env.fingerprintAvailable = true
|
||||
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
||||
env.writePamFile(t, "greetd", original)
|
||||
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
|
||||
|
||||
settings := AuthSettings{GreeterEnableFprint: true}
|
||||
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
|
||||
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readFileString(t, env.greetdPath)
|
||||
if got != original {
|
||||
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
|
||||
}
|
||||
if strings.Contains(got, GreeterPamManagedBlockStart) {
|
||||
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
|
||||
legacyGreeterPamFprintComment+"\n"+
|
||||
"auth sufficient pam_fprintd.so max-tries=1\n"+
|
||||
GreeterPamManagedBlockStart+"\n"+
|
||||
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
|
||||
GreeterPamManagedBlockEnd+"\n"+
|
||||
"auth include system-auth\n")
|
||||
|
||||
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
|
||||
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
got := readFileString(t, env.greetdPath)
|
||||
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
|
||||
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, "auth include system-auth") {
|
||||
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncAuthConfigWithDeps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.writeSettings(t, `{"enableU2f":true}`)
|
||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||
|
||||
var logs []string
|
||||
err := syncAuthConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(env.dankshellPath); err != nil {
|
||||
t.Fatalf("expected dankshell to be created: %v", err)
|
||||
}
|
||||
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
|
||||
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
|
||||
}
|
||||
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
|
||||
t.Fatalf("expected greetd skip log, got %v", logs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.availableModules["pam_fprintd.so"] = true
|
||||
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
|
||||
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
|
||||
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
|
||||
|
||||
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
|
||||
if err != nil {
|
||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
dankshell := readFileString(t, env.dankshellPath)
|
||||
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
|
||||
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
|
||||
}
|
||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
|
||||
}
|
||||
|
||||
greetd := readFileString(t, env.greetdPath)
|
||||
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") {
|
||||
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
|
||||
}
|
||||
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
|
||||
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newPamTestEnv(t)
|
||||
env.availableModules["pam_fprintd.so"] = true
|
||||
env.availableModules["pam_u2f.so"] = true
|
||||
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
|
||||
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
|
||||
env.writePamFile(t, "greetd", originalGreetd)
|
||||
|
||||
var logs []string
|
||||
err := syncAuthConfigWithDeps(func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
|
||||
if err != nil {
|
||||
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
|
||||
}
|
||||
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
|
||||
}
|
||||
if got := readFileString(t, env.greetdPath); got != originalGreetd {
|
||||
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
|
||||
}
|
||||
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
|
||||
t.Fatalf("expected informational NixOS logs, got %v", logs)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
|
||||
|
||||
type outputInfo struct {
|
||||
x, y int32
|
||||
scale float64
|
||||
transform int32
|
||||
}
|
||||
|
||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
func getAllOutputInfos() map[string]*outputInfo {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
ctx := display.Context()
|
||||
defer ctx.Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
})
|
||||
|
||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
if outputManager == nil {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
type headState struct {
|
||||
name string
|
||||
x, y int32
|
||||
scale float64
|
||||
transform int32
|
||||
}
|
||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
state.x = pe.X
|
||||
state.y = pe.Y
|
||||
})
|
||||
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||
state.scale = se.Scale
|
||||
})
|
||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||
state.transform = te.Transform
|
||||
})
|
||||
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
|
||||
for !done {
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
result := make(map[string]*outputInfo, len(heads))
|
||||
for _, state := range heads {
|
||||
if state.name == outputName {
|
||||
return &outputInfo{
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
transform: state.transform,
|
||||
}, true
|
||||
if state.name == "" {
|
||||
continue
|
||||
}
|
||||
result[state.name] = &outputInfo{
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
scale: state.scale,
|
||||
transform: state.transform,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return nil, false
|
||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||
infos := getAllOutputInfos()
|
||||
if infos == nil {
|
||||
return nil, false
|
||||
}
|
||||
info, ok := infos[outputName]
|
||||
return info, ok
|
||||
}
|
||||
|
||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||
|
||||
@@ -113,7 +113,11 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||
}
|
||||
|
||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
||||
r.preSelect = GetLastRegion()
|
||||
if r.screenshoter != nil && r.screenshoter.config.Reset {
|
||||
r.preSelect = Region{}
|
||||
} else {
|
||||
r.preSelect = GetLastRegion()
|
||||
}
|
||||
|
||||
if err := r.connect(); err != nil {
|
||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||
|
||||
@@ -114,6 +114,9 @@ func (r *RegionSelector) setupPointerHandlers() {
|
||||
for _, os := range r.surfaces {
|
||||
r.redrawSurface(os)
|
||||
}
|
||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
|
||||
r.finishSelection()
|
||||
}
|
||||
}
|
||||
default:
|
||||
r.cancelled = true
|
||||
|
||||
@@ -138,9 +138,13 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
|
||||
if !r.showCapturedCursor {
|
||||
cursorLabel = "show"
|
||||
}
|
||||
captureKey := "Space/Enter"
|
||||
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
||||
captureKey = "Drag+Release"
|
||||
}
|
||||
|
||||
items := []struct{ key, desc string }{
|
||||
{"Space/Enter", "capture"},
|
||||
{captureKey, "capture"},
|
||||
{"P", cursorLabel + " cursor"},
|
||||
{"Esc", "cancel"},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package screenshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -106,6 +107,12 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
|
||||
if s.config.Reset {
|
||||
if err := SaveLastRegion(Region{}); err != nil {
|
||||
log.Debug("failed to reset last region", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
selector := NewRegionSelector(s)
|
||||
result, cancelled, err := selector.Run()
|
||||
if err != nil {
|
||||
@@ -298,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
if len(outputs) == 0 {
|
||||
return nil, fmt.Errorf("no outputs available")
|
||||
}
|
||||
|
||||
if len(outputs) == 1 {
|
||||
return s.captureWholeOutput(outputs[0])
|
||||
}
|
||||
|
||||
// Capture all outputs first to get actual buffer sizes
|
||||
type capturedOutput struct {
|
||||
output *WaylandOutput
|
||||
result *CaptureResult
|
||||
physX int
|
||||
physY int
|
||||
}
|
||||
captured := make([]capturedOutput, 0, len(outputs))
|
||||
wlrInfos := getAllOutputInfos()
|
||||
|
||||
var minX, minY, maxX, maxY int
|
||||
first := true
|
||||
type pendingOutput struct {
|
||||
result *CaptureResult
|
||||
logX float64
|
||||
logY float64
|
||||
scale float64
|
||||
}
|
||||
var pending []pendingOutput
|
||||
maxScale := 1.0
|
||||
|
||||
for _, output := range outputs {
|
||||
result, err := s.captureWholeOutput(output)
|
||||
@@ -322,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
outX, outY := output.x, output.y
|
||||
logX, logY := float64(output.x), float64(output.y)
|
||||
scale := float64(output.scale)
|
||||
|
||||
switch DetectCompositor() {
|
||||
case CompositorHyprland:
|
||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||
outX, outY = hx, hy
|
||||
logX, logY = float64(hx), float64(hy)
|
||||
}
|
||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
||||
scale = s
|
||||
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||
scale = hs
|
||||
}
|
||||
case CompositorDWL:
|
||||
if info, ok := getOutputInfo(output.name); ok {
|
||||
outX, outY = info.x, info.y
|
||||
default:
|
||||
if wlrInfos != nil {
|
||||
if info, ok := wlrInfos[output.name]; ok {
|
||||
logX, logY = float64(info.x), float64(info.y)
|
||||
if info.scale > 0 {
|
||||
scale = info.scale
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scale <= 0 {
|
||||
scale = 1.0
|
||||
}
|
||||
|
||||
physX := int(float64(outX) * scale)
|
||||
physY := int(float64(outY) * scale)
|
||||
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||
if scale > maxScale {
|
||||
maxScale = scale
|
||||
}
|
||||
}
|
||||
|
||||
captured = append(captured, capturedOutput{
|
||||
output: output,
|
||||
result: result,
|
||||
physX: physX,
|
||||
physY: physY,
|
||||
})
|
||||
if len(pending) == 0 {
|
||||
return nil, fmt.Errorf("failed to capture any outputs")
|
||||
}
|
||||
if len(pending) == 1 {
|
||||
return pending[0].result, nil
|
||||
}
|
||||
|
||||
right := physX + result.Buffer.Width
|
||||
bottom := physY + result.Buffer.Height
|
||||
type layoutEntry struct {
|
||||
result *CaptureResult
|
||||
canvasX int
|
||||
canvasY int
|
||||
canvasW int
|
||||
canvasH int
|
||||
}
|
||||
entries := make([]layoutEntry, len(pending))
|
||||
var minX, minY, maxX, maxY int
|
||||
|
||||
if first {
|
||||
minX, minY = physX, physY
|
||||
maxX, maxY = right, bottom
|
||||
first = false
|
||||
for i, p := range pending {
|
||||
cx := int(math.Round(p.logX * maxScale))
|
||||
cy := int(math.Round(p.logY * maxScale))
|
||||
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||
|
||||
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||
|
||||
right := cx + cw
|
||||
bottom := cy + ch
|
||||
if i == 0 {
|
||||
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||
continue
|
||||
}
|
||||
|
||||
if physX < minX {
|
||||
minX = physX
|
||||
if cx < minX {
|
||||
minX = cx
|
||||
}
|
||||
if physY < minY {
|
||||
minY = physY
|
||||
if cy < minY {
|
||||
minY = cy
|
||||
}
|
||||
if right > maxX {
|
||||
maxX = right
|
||||
@@ -375,35 +404,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(captured) == 0 {
|
||||
return nil, fmt.Errorf("failed to capture any outputs")
|
||||
}
|
||||
|
||||
if len(captured) == 1 {
|
||||
return captured[0].result, nil
|
||||
}
|
||||
|
||||
totalW := maxX - minX
|
||||
totalH := maxY - minY
|
||||
|
||||
compositeStride := totalW * 4
|
||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
||||
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||
if err != nil {
|
||||
for _, c := range captured {
|
||||
c.result.Buffer.Close()
|
||||
for _, e := range entries {
|
||||
e.result.Buffer.Close()
|
||||
}
|
||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||
}
|
||||
|
||||
composite.Clear()
|
||||
|
||||
var format uint32
|
||||
for _, c := range captured {
|
||||
for _, e := range entries {
|
||||
if format == 0 {
|
||||
format = c.result.Format
|
||||
format = e.result.Format
|
||||
}
|
||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
||||
c.result.Buffer.Close()
|
||||
s.blitBufferScaled(composite, e.result.Buffer,
|
||||
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||
e.result.YInverted)
|
||||
e.result.Buffer.Close()
|
||||
}
|
||||
|
||||
return &CaptureResult{
|
||||
@@ -413,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
||||
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||
if dstW <= 0 || dstH <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
srcData := src.Data()
|
||||
dstData := dst.Data()
|
||||
|
||||
for srcY := 0; srcY < src.Height; srcY++ {
|
||||
actualSrcY := srcY
|
||||
if yInverted {
|
||||
actualSrcY = src.Height - 1 - srcY
|
||||
}
|
||||
|
||||
dy := dstY + srcY
|
||||
if dy < 0 || dy >= dst.Height {
|
||||
for dy := 0; dy < dstH; dy++ {
|
||||
canvasY := dstY + dy
|
||||
if canvasY < 0 || canvasY >= dst.Height {
|
||||
continue
|
||||
}
|
||||
|
||||
srcRowOff := actualSrcY * src.Stride
|
||||
dstRowOff := dy * dst.Stride
|
||||
srcY := dy * src.Height / dstH
|
||||
if yInverted {
|
||||
srcY = src.Height - 1 - srcY
|
||||
}
|
||||
if srcY < 0 || srcY >= src.Height {
|
||||
continue
|
||||
}
|
||||
|
||||
for srcX := 0; srcX < src.Width; srcX++ {
|
||||
dx := dstX + srcX
|
||||
if dx < 0 || dx >= dst.Width {
|
||||
srcRowOff := srcY * src.Stride
|
||||
dstRowOff := canvasY * dst.Stride
|
||||
|
||||
for dx := 0; dx < dstW; dx++ {
|
||||
canvasX := dstX + dx
|
||||
if canvasX < 0 || canvasX >= dst.Width {
|
||||
continue
|
||||
}
|
||||
|
||||
srcX := dx * src.Width / dstW
|
||||
if srcX >= src.Width {
|
||||
continue
|
||||
}
|
||||
|
||||
si := srcRowOff + srcX*4
|
||||
di := dstRowOff + dx*4
|
||||
di := dstRowOff + canvasX*4
|
||||
|
||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||
continue
|
||||
|
||||
@@ -52,6 +52,8 @@ type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
NoConfirm bool
|
||||
Reset bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
@@ -66,6 +68,8 @@ func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeRegion,
|
||||
Cursor: CursorOff,
|
||||
NoConfirm: false,
|
||||
Reset: false,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
|
||||
@@ -29,6 +29,7 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
||||
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||
Contrast: models.GetOr(req, "contrast", 0.0),
|
||||
}
|
||||
|
||||
wait := models.GetOr(req, "wait", true)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||
|
||||
Package: dms
|
||||
Architecture: amd64
|
||||
Architecture: amd64 arm64
|
||||
Depends: ${misc:Depends},
|
||||
quickshell | quickshell-git,
|
||||
accountsservice,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
dms-distropkg-amd64.gz
|
||||
dms-distropkg-arm64.gz
|
||||
dms-source.tar.gz
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Include files that are normally excluded by .gitignore
|
||||
# These are needed for the build process on Launchpad
|
||||
tar-ignore = !dms-distropkg-amd64.gz
|
||||
tar-ignore = !dms-distropkg-arm64.gz
|
||||
tar-ignore = !dms-source.tar.gz
|
||||
|
||||
@@ -139,6 +139,13 @@ in
|
||||
'';
|
||||
}
|
||||
];
|
||||
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
|
||||
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
|
||||
# U2F and fingerprint are handled separately by DMS — do not add pam_u2f or pam_fprintd here.
|
||||
# security.pam.services.dankshell = {
|
||||
# # Example: add faillock
|
||||
# faillock.enable = true;
|
||||
# };
|
||||
services.greetd = {
|
||||
enable = lib.mkDefault true;
|
||||
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
|
||||
#
|
||||
# Example:
|
||||
# ./create-source.sh ../dms questing
|
||||
# ./create-source.sh ../dms questing # Ubuntu 25.10 (default series in ppa-upload)
|
||||
# ./create-source.sh ../dms resolute # Ubuntu 26.04 LTS
|
||||
# ./create-source.sh ../dms-git questing
|
||||
# ./create-source.sh ../dms-git resolute
|
||||
|
||||
set -e
|
||||
|
||||
@@ -25,11 +27,13 @@ if [ $# -lt 1 ]; then
|
||||
echo "Arguments:"
|
||||
echo " package-dir : Path to package directory (e.g., ../dms)"
|
||||
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
|
||||
echo " Options: noble, jammy, oracular, mantic"
|
||||
echo " Options: noble, jammy, oracular, mantic, questing, resolute"
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " $0 ../dms questing"
|
||||
echo " $0 ../dms resolute"
|
||||
echo " $0 ../dms-git questing"
|
||||
echo " $0 ../dms-git resolute"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -129,10 +133,14 @@ check_ppa_version_exists() {
|
||||
local SOURCE_NAME="$2"
|
||||
local VERSION="$3"
|
||||
local CHECK_MODE="${4:-commit}"
|
||||
local DISTRO_SERIES="${5:-}"
|
||||
|
||||
# Query Launchpad API
|
||||
PPA_VERSION=$(curl -s \
|
||||
"https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" \
|
||||
# Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to questing and resolute)
|
||||
local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
|
||||
if [[ -n "$DISTRO_SERIES" ]]; then
|
||||
API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}"
|
||||
fi
|
||||
PPA_VERSION=$(curl -s "$API_URL" \
|
||||
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||
|
||||
if [[ -n "$PPA_VERSION" ]]; then
|
||||
@@ -259,14 +267,14 @@ if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
|
||||
if [[ -n "$PPA_NAME" ]]; then
|
||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact"; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact" "$UBUNTU_SERIES"; then
|
||||
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
|
||||
error " To rebuild with a different release number, use:"
|
||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
|
||||
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
|
||||
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
||||
error " To rebuild with a different release number, use:"
|
||||
@@ -410,7 +418,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
if [[ -n "$PPA_NAME" ]]; then
|
||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit"; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit" "$UBUNTU_SERIES"; then
|
||||
error "==> Error: This commit is already uploaded to PPA"
|
||||
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
||||
error " To rebuild the same commit, specify a rebuild number:"
|
||||
@@ -429,7 +437,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
PPA_NUM=$REBUILD_RELEASE
|
||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then
|
||||
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
|
||||
error "==> Error: Version $NEW_VERSION already exists in PPA"
|
||||
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
||||
NEXT_NUM=$((PPA_NUM + 1))
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
PPA_OWNER="avengemedia"
|
||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
||||
DISTRO_SERIES="questing"
|
||||
# Supported Ubuntu series for PPA builds (25.10 questing + 26.04 LTS resolute)
|
||||
DISTRO_SERIES_LIST=(questing resolute)
|
||||
|
||||
# Define packages (sync with ppa-upload.sh)
|
||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||
@@ -106,10 +107,10 @@ get_status_display() {
|
||||
for PPA_NAME in "${PPAS[@]}"; do
|
||||
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||
|
||||
for DISTRO_SERIES in "${DISTRO_SERIES_LIST[@]}"; do
|
||||
echo "=========================================="
|
||||
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ==="
|
||||
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} (Ubuntu ${DISTRO_SERIES}) ==="
|
||||
echo "=========================================="
|
||||
echo "Distribution: Ubuntu $DISTRO_SERIES"
|
||||
echo ""
|
||||
|
||||
for pkg in "${PACKAGES[@]}"; do
|
||||
@@ -210,6 +211,7 @@ for PPA_NAME in "${PPAS[@]}"; do
|
||||
|
||||
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||
echo ""
|
||||
done
|
||||
done
|
||||
|
||||
echo "=========================================="
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
||||
#
|
||||
# Examples:
|
||||
# ./ppa-upload.sh dms # Single package (auto-detects PPA)
|
||||
# ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax)
|
||||
# ./ppa-upload.sh dms # Upload to questing + resolute (default)
|
||||
# ./ppa-upload.sh dms 2 # Native: questing ppa2, resolute ppa3 (auto +1 on second series)
|
||||
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
|
||||
# ./ppa-upload.sh dms-git # Single package
|
||||
# ./ppa-upload.sh all # All packages
|
||||
# ./ppa-upload.sh dms dms questing # Explicit PPA and series
|
||||
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number
|
||||
# ./ppa-upload.sh dms-git # Single package (both series)
|
||||
# ./ppa-upload.sh all # All packages (each to both series)
|
||||
# ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute")
|
||||
# ./ppa-upload.sh dms questing # 25.10 only
|
||||
# ./ppa-upload.sh dms dms resolute # Explicit PPA name + one series (optional form)
|
||||
# ./ppa-upload.sh dms dms resolute 2 # One series + rebuild number
|
||||
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
|
||||
|
||||
set -e
|
||||
@@ -52,7 +54,7 @@ done
|
||||
|
||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
||||
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
||||
@@ -64,10 +66,27 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Shorthand: "dms resolute" / "dms questing" (package + series; PPA inferred — no need for "dms dms resolute")
|
||||
if [[ ${#POSITIONAL_ARGS[@]} -eq 2 ]] && [[ "${POSITIONAL_ARGS[1]}" == "questing" || "${POSITIONAL_ARGS[1]}" == "resolute" ]]; then
|
||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]}"
|
||||
PPA_NAME_INPUT=""
|
||||
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[1]}"
|
||||
fi
|
||||
|
||||
SERIES_LIST=()
|
||||
if [[ -z "$UBUNTU_SERIES_RAW" ]]; then
|
||||
SERIES_LIST=(questing resolute)
|
||||
elif [[ "$UBUNTU_SERIES_RAW" == "questing" || "$UBUNTU_SERIES_RAW" == "resolute" ]]; then
|
||||
SERIES_LIST=("$UBUNTU_SERIES_RAW")
|
||||
else
|
||||
error "Invalid Ubuntu series: $UBUNTU_SERIES_RAW (use questing, resolute, or omit for both)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
||||
@@ -119,7 +138,12 @@ elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
info "Processing $pkg..."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
BUILD_ARGS=("$pkg" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||
BUILD_ARGS=("$pkg")
|
||||
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
|
||||
if [[ ${#SERIES_LIST[@]} -eq 1 ]]; then
|
||||
BUILD_ARGS+=("${SERIES_LIST[0]}")
|
||||
fi
|
||||
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
|
||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||
if ! "$0" "${BUILD_ARGS[@]}"; then
|
||||
FAILED_PACKAGES+=("$pkg")
|
||||
@@ -165,7 +189,9 @@ else
|
||||
|
||||
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
||||
PACKAGE_INPUT="all"
|
||||
BUILD_ARGS=("all" "$PPA_NAME_INPUT" "$UBUNTU_SERIES")
|
||||
BUILD_ARGS=("all")
|
||||
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
|
||||
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
|
||||
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||
exec "$0" "${BUILD_ARGS[@]}"
|
||||
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
||||
@@ -191,6 +217,48 @@ fi
|
||||
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
||||
|
||||
if [[ ${#SERIES_LIST[@]} -gt 1 ]]; then
|
||||
SOURCE_FORMAT_LINE=$(head -1 "$PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "")
|
||||
IS_NATIVE_DUAL=false
|
||||
if [[ "$SOURCE_FORMAT_LINE" == *"native"* ]]; then
|
||||
IS_NATIVE_DUAL=true
|
||||
info "Native source format: second series uses PPA suffix +1 (or ppa2 if unset) so both uploads succeed."
|
||||
fi
|
||||
export REBUILD_RELEASE
|
||||
for idx in "${!SERIES_LIST[@]}"; do
|
||||
SERIES="${SERIES_LIST[$idx]}"
|
||||
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == *"/"* ]]; then
|
||||
ARGS=("$PACKAGE_DIR" "$PPA_NAME" "$SERIES")
|
||||
else
|
||||
ARGS=("$PACKAGE_NAME" "$PPA_NAME" "$SERIES")
|
||||
fi
|
||||
if [[ "$IS_NATIVE_DUAL" == true ]]; then
|
||||
if [[ "$idx" -eq 0 ]]; then
|
||||
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
|
||||
else
|
||||
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
|
||||
SECOND_PPA=$((REBUILD_RELEASE + 1))
|
||||
ARGS+=("$SECOND_PPA")
|
||||
info "Second series ${SERIES}: using ppa${SECOND_PPA} (native dual-series)"
|
||||
else
|
||||
ARGS+=("2")
|
||||
info "Second series ${SERIES}: using ppa2 (native dual-series; first uses default ppa1)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
|
||||
fi
|
||||
[[ "$KEEP_BUILDS" == "true" ]] && ARGS+=("--keep-builds")
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
info "Upload series: $SERIES (of ${SERIES_LIST[*]})"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
"$0" "${ARGS[@]}" || exit 1
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
UBUNTU_SERIES="${SERIES_LIST[0]}"
|
||||
|
||||
info "Building and uploading: $PACKAGE_NAME"
|
||||
info "Package directory: $PACKAGE_DIR"
|
||||
info "PPA: ppa:avengemedia/$PPA_NAME"
|
||||
|
||||
@@ -538,6 +538,8 @@ Color picker modal control.
|
||||
|
||||
**Functions:**
|
||||
- `open` - Show color picker modal
|
||||
- `openColor <color>` - Show color picker modal with a pre-selected color
|
||||
- Parameters: `color` - Color string (e.g. "#ff0000", "#3f51b5")
|
||||
- `close` - Hide color picker modal
|
||||
- `closeInstant` - Hide color picker modal without animation
|
||||
- `toggle` - Toggle color picker modal visibility
|
||||
|
||||
@@ -150,6 +150,9 @@
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||
|
||||
substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
|
||||
--replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.so
|
||||
|
||||
installShellCompletion --cmd dms \
|
||||
--bash <($out/bin/dms completion bash) \
|
||||
--fish <($out/bin/dms completion fish) \
|
||||
|
||||
@@ -10,6 +10,7 @@ Singleton {
|
||||
|
||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property url xdgCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||
|
||||
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||
@@ -72,7 +73,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function resolveIconPath(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
if (!iconName)
|
||||
return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
@@ -85,7 +87,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
if (!iconName)
|
||||
return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
@@ -98,7 +101,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||
if (appId === "org.quickshell") {
|
||||
// ! TODO - after QS 0.3, we can install our icon properly
|
||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||
}
|
||||
|
||||
@@ -118,7 +122,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
if (appId === "org.quickshell") {
|
||||
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
||||
return "dms";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
@@ -12,6 +13,37 @@ Singleton {
|
||||
signal popoutOpening
|
||||
signal popoutChanged
|
||||
|
||||
function _closePopout(popout) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function _isStale(popout) {
|
||||
try {
|
||||
if (!popout || !("shouldBeVisible" in popout))
|
||||
return true;
|
||||
if (!popout.screen)
|
||||
return true;
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function showPopout(popout) {
|
||||
if (!popout || !popout.screen)
|
||||
return;
|
||||
@@ -23,13 +55,11 @@ Singleton {
|
||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||
if (!otherPopout || otherPopout === popout)
|
||||
continue;
|
||||
if (otherPopout.dashVisible !== undefined) {
|
||||
otherPopout.dashVisible = false;
|
||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||
otherPopout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
otherPopout.close();
|
||||
if (_isStale(otherPopout)) {
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
continue;
|
||||
}
|
||||
_closePopout(otherPopout);
|
||||
}
|
||||
|
||||
currentPopoutsByScreen[screenName] = popout;
|
||||
@@ -51,15 +81,9 @@ Singleton {
|
||||
function closeAllPopouts() {
|
||||
for (const screenName in currentPopoutsByScreen) {
|
||||
const popout = currentPopoutsByScreen[screenName];
|
||||
if (!popout)
|
||||
if (!popout || _isStale(popout))
|
||||
continue;
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
_closePopout(popout);
|
||||
}
|
||||
currentPopoutsByScreen = {};
|
||||
}
|
||||
@@ -90,6 +114,12 @@ Singleton {
|
||||
if (!otherPopout)
|
||||
continue;
|
||||
|
||||
if (_isStale(otherPopout)) {
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
currentPopoutTriggers[otherScreenName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherPopout === popout) {
|
||||
movedFromOtherScreen = true;
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
@@ -97,45 +127,26 @@ Singleton {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherPopout.dashVisible !== undefined) {
|
||||
otherPopout.dashVisible = false;
|
||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
||||
otherPopout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
otherPopout.close();
|
||||
}
|
||||
_closePopout(otherPopout);
|
||||
}
|
||||
|
||||
if (currentPopout && currentPopout !== popout) {
|
||||
if (currentPopout.dashVisible !== undefined) {
|
||||
currentPopout.dashVisible = false;
|
||||
} else if (currentPopout.notificationHistoryVisible !== undefined) {
|
||||
currentPopout.notificationHistoryVisible = false;
|
||||
if (_isStale(currentPopout)) {
|
||||
currentPopoutsByScreen[screenName] = null;
|
||||
currentPopoutTriggers[screenName] = null;
|
||||
} else {
|
||||
currentPopout.close();
|
||||
_closePopout(currentPopout);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
_closePopout(popout);
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerId === undefined) {
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = false;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = false;
|
||||
} else {
|
||||
popout.close();
|
||||
}
|
||||
_closePopout(popout);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,8 +132,12 @@ Singleton {
|
||||
property string timeLocale: ""
|
||||
|
||||
property string launcherLastMode: "all"
|
||||
property string launcherLastQuery: ""
|
||||
property var launcherQueryHistory: []
|
||||
property string appDrawerLastMode: "apps"
|
||||
property string niriOverviewLastMode: "apps"
|
||||
property string settingsSidebarExpandedIds: ","
|
||||
property string settingsSidebarCollapsedIds: ","
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isGreeterMode) {
|
||||
@@ -343,8 +347,8 @@ Singleton {
|
||||
|
||||
function setLightMode(lightMode) {
|
||||
isSwitchingMode = true;
|
||||
syncWallpaperForCurrentMode(lightMode);
|
||||
isLightMode = lightMode;
|
||||
syncWallpaperForCurrentMode();
|
||||
saveSettings();
|
||||
Qt.callLater(() => {
|
||||
isSwitchingMode = false;
|
||||
@@ -1094,6 +1098,43 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setLauncherLastQuery(query) {
|
||||
launcherLastQuery = query;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function addLauncherHistory(query) {
|
||||
let q = query.trim();
|
||||
|
||||
setLauncherLastQuery(q);
|
||||
|
||||
if (!q)
|
||||
return;
|
||||
|
||||
if (launcherQueryHistory.length > 0 && launcherQueryHistory[0] === q) {
|
||||
return;
|
||||
}
|
||||
|
||||
let history = [...launcherQueryHistory];
|
||||
|
||||
let idx = history.indexOf(q);
|
||||
if (idx !== -1)
|
||||
history.splice(idx, 1);
|
||||
|
||||
history.unshift(q);
|
||||
if (history.length > 50)
|
||||
history = history.slice(0, 50);
|
||||
|
||||
launcherQueryHistory = history;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function clearLauncherHistory() {
|
||||
launcherLastQuery = "";
|
||||
launcherSearchHistory = [];
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setAppDrawerLastMode(mode) {
|
||||
appDrawerLastMode = mode;
|
||||
saveSettings();
|
||||
@@ -1104,15 +1145,22 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode() {
|
||||
function setSettingsSidebarState(expandedIds, collapsedIds) {
|
||||
settingsSidebarExpandedIds = expandedIds;
|
||||
settingsSidebarCollapsedIds = collapsedIds;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function syncWallpaperForCurrentMode(mode) {
|
||||
if (!perModeWallpaper)
|
||||
return;
|
||||
var light = (mode !== undefined) ? mode : isLightMode;
|
||||
if (perMonitorWallpaper) {
|
||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
||||
monitorWallpapers = light ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
||||
return;
|
||||
}
|
||||
|
||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark;
|
||||
wallpaperPath = light ? wallpaperPathLight : wallpaperPathDark;
|
||||
}
|
||||
|
||||
function _findMonitorValue(map, screenName) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 6
|
||||
readonly property int settingsConfigVersion: 11
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -130,6 +130,7 @@ Singleton {
|
||||
property string customThemeFile: ""
|
||||
property var registryThemeVariants: ({})
|
||||
property string matugenScheme: "scheme-tonal-spot"
|
||||
property real matugenContrast: 0
|
||||
property bool runUserMatugenTemplates: true
|
||||
property string matugenTargetMonitor: ""
|
||||
property real popupTransparency: 1.0
|
||||
@@ -150,6 +151,7 @@ Singleton {
|
||||
property int mangoLayoutBorderSize: -1
|
||||
|
||||
property int firstDayOfWeek: -1
|
||||
property bool showWeekNumber: false
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
@@ -184,10 +186,46 @@ Singleton {
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
|
||||
property bool blurEnabled: false
|
||||
onBlurEnabledChanged: saveSettings()
|
||||
property string blurBorderColor: "outline"
|
||||
onBlurBorderColorChanged: saveSettings()
|
||||
property string blurBorderCustomColor: "#ffffff"
|
||||
onBlurBorderCustomColorChanged: saveSettings()
|
||||
property real blurBorderOpacity: 1.0
|
||||
onBlurBorderOpacityChanged: saveSettings()
|
||||
property string wallpaperFillMode: "Fill"
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
|
||||
property bool frameEnabled: false
|
||||
onFrameEnabledChanged: saveSettings()
|
||||
property real frameThickness: 16
|
||||
onFrameThicknessChanged: saveSettings()
|
||||
property real frameRounding: 23
|
||||
onFrameRoundingChanged: saveSettings()
|
||||
property string frameColor: ""
|
||||
onFrameColorChanged: saveSettings()
|
||||
property real frameOpacity: 1.0
|
||||
onFrameOpacityChanged: saveSettings()
|
||||
property var frameScreenPreferences: ["all"]
|
||||
onFrameScreenPreferencesChanged: saveSettings()
|
||||
property real frameBarSize: 40
|
||||
onFrameBarSizeChanged: saveSettings()
|
||||
property bool frameShowOnOverview: false
|
||||
onFrameShowOnOverviewChanged: saveSettings()
|
||||
property bool frameBlurEnabled: true
|
||||
onFrameBlurEnabledChanged: saveSettings()
|
||||
|
||||
readonly property color effectiveFrameColor: {
|
||||
const fc = frameColor;
|
||||
if (!fc || fc === "default") return Theme.surfaceContainer;
|
||||
if (fc === "primary") return Theme.primary;
|
||||
if (fc === "surface") return Theme.surface;
|
||||
return fc;
|
||||
}
|
||||
|
||||
property bool showLauncherButton: true
|
||||
property bool showWorkspaceSwitcher: true
|
||||
property bool showFocusedWindow: true
|
||||
@@ -336,6 +374,7 @@ Singleton {
|
||||
property bool sortAppsAlphabetically: false
|
||||
property int appLauncherGridColumns: 4
|
||||
property bool spotlightCloseNiriOverview: true
|
||||
property bool rememberLastQuery: false
|
||||
property var spotlightSectionViewModes: ({})
|
||||
onSpotlightSectionViewModesChanged: saveSettings()
|
||||
property var appDrawerSectionViewModes: ({})
|
||||
@@ -453,6 +492,11 @@ Singleton {
|
||||
property bool syncModeWithPortal: true
|
||||
property bool terminalsAlwaysDark: false
|
||||
|
||||
property string muxType: "tmux"
|
||||
property bool muxUseCustomCommand: false
|
||||
property string muxCustomCommand: ""
|
||||
property string muxSessionFilter: ""
|
||||
|
||||
property bool runDmsMatugenTemplates: true
|
||||
property bool matugenTemplateGtk: true
|
||||
property bool matugenTemplateNiri: true
|
||||
@@ -478,9 +522,16 @@ Singleton {
|
||||
property bool matugenTemplateZed: true
|
||||
|
||||
property var matugenTemplateNeovimSettings: ({
|
||||
"dark": { "baseTheme": "github_dark", "harmony": 0.5 },
|
||||
"light": { "baseTheme": "github_light", "harmony": 0.5 }
|
||||
})
|
||||
"dark": {
|
||||
"baseTheme": "github_dark",
|
||||
"harmony": 0.5
|
||||
},
|
||||
"light": {
|
||||
"baseTheme": "github_light",
|
||||
"harmony": 0.5
|
||||
}
|
||||
})
|
||||
property bool matugenTemplateNeovimSetBackground: true
|
||||
|
||||
property bool showDock: false
|
||||
property bool dockAutoHide: false
|
||||
@@ -1189,13 +1240,23 @@ Singleton {
|
||||
Quickshell.execDetached(["sh", "-lc", script]);
|
||||
}
|
||||
|
||||
function scheduleAuthApply() {
|
||||
if (isGreeterMode)
|
||||
return;
|
||||
Qt.callLater(() => {
|
||||
Processes.settingsRoot = root;
|
||||
Processes.scheduleAuthApply();
|
||||
});
|
||||
}
|
||||
|
||||
readonly property var _hooks: ({
|
||||
"applyStoredTheme": applyStoredTheme,
|
||||
"regenSystemThemes": regenSystemThemes,
|
||||
"updateCompositorLayout": updateCompositorLayout,
|
||||
"applyStoredIconTheme": applyStoredIconTheme,
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
"scheduleAuthApply": scheduleAuthApply
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
@@ -1314,9 +1375,7 @@ Singleton {
|
||||
return true;
|
||||
|
||||
const msg = String(error || "").toLowerCase();
|
||||
return msg.indexOf("file does not exist") !== -1
|
||||
|| msg.indexOf("no such file") !== -1
|
||||
|| msg.indexOf("enoent") !== -1;
|
||||
return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1;
|
||||
}
|
||||
|
||||
function loadPluginSettings() {
|
||||
@@ -1907,6 +1966,66 @@ Singleton {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getFrameFilteredScreens() {
|
||||
var prefs = frameScreenPreferences || ["all"];
|
||||
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
||||
return Quickshell.screens;
|
||||
}
|
||||
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||
}
|
||||
|
||||
function getActiveBarEdgeForScreen(screen) {
|
||||
if (!screen) return "";
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top: return "top";
|
||||
case SettingsData.Position.Bottom: return "bottom";
|
||||
case SettingsData.Position.Left: return "left";
|
||||
case SettingsData.Position.Right: return "right";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getActiveBarEdgesForScreen(screen) {
|
||||
if (!screen) return [];
|
||||
var edges = [];
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top: edges.push("top"); break;
|
||||
case SettingsData.Position.Bottom: edges.push("bottom"); break;
|
||||
case SettingsData.Position.Left: edges.push("left"); break;
|
||||
case SettingsData.Position.Right: edges.push("right"); break;
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
function getActiveBarThicknessForScreen(screen) {
|
||||
if (frameEnabled) return frameBarSize;
|
||||
if (!screen) return frameThickness;
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
const innerPadding = bc.innerPadding ?? 4;
|
||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||
const spacing = bc.spacing ?? 4;
|
||||
const bottomGap = bc.bottomGap ?? 0;
|
||||
return barT + spacing + bottomGap;
|
||||
}
|
||||
return frameThickness;
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
NotificationService.dismissAllPopups();
|
||||
sendTestNotification(0);
|
||||
@@ -1936,6 +2055,12 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function setMatugenContrast(value) {
|
||||
if (matugenContrast === value)
|
||||
return;
|
||||
set("matugenContrast", value);
|
||||
}
|
||||
|
||||
function setRunUserMatugenTemplates(enabled) {
|
||||
if (runUserMatugenTemplates === enabled)
|
||||
return;
|
||||
|
||||
@@ -1248,7 +1248,8 @@ Singleton {
|
||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||
const defaults = themeData.variants.defaults || {};
|
||||
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 || "";
|
||||
const accentId = stored.accent || modeDefaults.accent || "";
|
||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||
@@ -1274,7 +1275,8 @@ Singleton {
|
||||
}
|
||||
|
||||
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);
|
||||
if (variant) {
|
||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||
@@ -1547,6 +1549,9 @@ Singleton {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||
args.push("--terminals-always-dark");
|
||||
}
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
|
||||
args.push("--contrast", SettingsData.matugenContrast.toString());
|
||||
}
|
||||
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const skipTemplates = [];
|
||||
@@ -1646,8 +1651,9 @@ Singleton {
|
||||
const defaults = customThemeRawData.variants.defaults || {};
|
||||
const darkDefaults = defaults.dark || {};
|
||||
const lightDefaults = defaults.light || defaults.dark || {};
|
||||
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
||||
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||
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 lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||
@@ -1665,7 +1671,8 @@ Singleton {
|
||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||
}
|
||||
} 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);
|
||||
if (variant) {
|
||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||
@@ -1993,6 +2000,7 @@ Singleton {
|
||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||
return colorsPath;
|
||||
}
|
||||
blockLoading: false
|
||||
watchChanges: !SessionData.isGreeterMode
|
||||
|
||||
function parseAndLoadColors() {
|
||||
|
||||
@@ -1249,7 +1249,7 @@ const defaultOpts = {
|
||||
};
|
||||
class Finder {
|
||||
constructor(list, ...optionsTuple) {
|
||||
this.opts = Object.assign(defaultOpts, optionsTuple[0]);
|
||||
this.opts = Object.assign({}, defaultOpts, optionsTuple[0]);
|
||||
this.items = list;
|
||||
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
|
||||
this.algoFn = exactMatchNaive;
|
||||
@@ -1283,12 +1283,13 @@ function postProcessResultItems(result, opts) {
|
||||
if (opts.sort) {
|
||||
const { selector } = opts;
|
||||
result.sort((a, b) => {
|
||||
if (a.score === b.score) {
|
||||
for (const tiebreaker of opts.tiebreakers) {
|
||||
const diff = tiebreaker(a, b, selector);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
if (a.score !== b.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
for (const tiebreaker of opts.tiebreakers) {
|
||||
const diff = tiebreaker(a, b, selector);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -52,6 +54,14 @@ Singleton {
|
||||
|
||||
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||
property bool authApplyRunning: false
|
||||
property bool authApplyQueued: false
|
||||
property bool authApplyRerunRequested: false
|
||||
property bool authApplyTerminalFallbackFromPrecheck: false
|
||||
property string authApplyStdout: ""
|
||||
property string authApplyStderr: ""
|
||||
property string authApplySudoProbeStderr: ""
|
||||
property string authApplyTerminalFallbackStderr: ""
|
||||
|
||||
function detectQtTools() {
|
||||
qtToolsDetectionProcess.running = true;
|
||||
@@ -70,14 +80,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();
|
||||
}
|
||||
@@ -94,6 +102,50 @@ Singleton {
|
||||
pluginSettingsCheckProcess.running = true;
|
||||
}
|
||||
|
||||
function scheduleAuthApply() {
|
||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
||||
return;
|
||||
|
||||
authApplyQueued = true;
|
||||
if (authApplyRunning) {
|
||||
authApplyRerunRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
authApplyDebounce.restart();
|
||||
}
|
||||
|
||||
function beginAuthApply() {
|
||||
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
||||
return;
|
||||
|
||||
authApplyQueued = false;
|
||||
authApplyRerunRequested = false;
|
||||
authApplyStdout = "";
|
||||
authApplyStderr = "";
|
||||
authApplySudoProbeStderr = "";
|
||||
authApplyTerminalFallbackStderr = "";
|
||||
authApplyTerminalFallbackFromPrecheck = false;
|
||||
authApplyRunning = true;
|
||||
authApplySudoProbeProcess.running = true;
|
||||
}
|
||||
|
||||
function launchAuthApplyTerminalFallback(fromPrecheck, details) {
|
||||
authApplyTerminalFallbackFromPrecheck = fromPrecheck;
|
||||
if (details && details !== "")
|
||||
ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
|
||||
authApplyTerminalFallbackStderr = "";
|
||||
authApplyTerminalFallbackProcess.running = true;
|
||||
}
|
||||
|
||||
function finishAuthApply() {
|
||||
const shouldRerun = authApplyQueued || authApplyRerunRequested;
|
||||
authApplyRunning = false;
|
||||
authApplyRerunRequested = false;
|
||||
if (shouldRerun)
|
||||
authApplyDebounce.restart();
|
||||
}
|
||||
|
||||
function stripPamComment(line) {
|
||||
if (!line)
|
||||
return "";
|
||||
@@ -419,6 +471,91 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: authApplyDebounce
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: root.beginAuthApply()
|
||||
}
|
||||
|
||||
property var authApplyProcess: Process {
|
||||
command: ["dms", "auth", "sync", "--yes"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.authApplyStdout = text || ""
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: root.authApplyStderr = text || ""
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
const out = (root.authApplyStdout || "").trim();
|
||||
const err = (root.authApplyStderr || "").trim();
|
||||
|
||||
if (exitCode === 0) {
|
||||
let details = out;
|
||||
if (err !== "")
|
||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||
ToastService.showInfo(I18n.tr("Authentication changes applied."), details, "", "auth-sync");
|
||||
root.detectAuthCapabilities();
|
||||
root.finishAuthApply();
|
||||
return;
|
||||
}
|
||||
|
||||
let details = "";
|
||||
if (out !== "")
|
||||
details = out;
|
||||
if (err !== "")
|
||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||
ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
|
||||
root.launchAuthApplyTerminalFallback(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
property var authApplySudoProbeProcess: Process {
|
||||
command: ["sudo", "-n", "true"]
|
||||
running: false
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: root.authApplySudoProbeStderr = text || ""
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
const err = (root.authApplySudoProbeStderr || "").trim();
|
||||
if (exitCode === 0) {
|
||||
ToastService.showInfo(I18n.tr("Applying authentication changes…"), "", "", "auth-sync");
|
||||
root.authApplyProcess.running = true;
|
||||
return;
|
||||
}
|
||||
|
||||
root.launchAuthApplyTerminalFallback(true, err);
|
||||
}
|
||||
}
|
||||
|
||||
property var authApplyTerminalFallbackProcess: Process {
|
||||
command: ["dms", "auth", "sync", "--terminal", "--yes"]
|
||||
running: false
|
||||
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: root.authApplyTerminalFallbackStderr = text || ""
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
const message = root.authApplyTerminalFallbackFromPrecheck
|
||||
? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.")
|
||||
: I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
|
||||
ToastService.showInfo(message, "", "", "auth-sync");
|
||||
} else {
|
||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
||||
ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
|
||||
}
|
||||
root.finishAuthApply();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: greetdPamWatcher
|
||||
path: "/etc/pam.d/greetd"
|
||||
|
||||
@@ -83,8 +83,13 @@ var SPEC = {
|
||||
timeLocale: { def: "" },
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
launcherLastQuery: { def: "" },
|
||||
launcherQueryHistory: { def: [] },
|
||||
appDrawerLastMode: { def: "apps" },
|
||||
niriOverviewLastMode: { def: "apps" }
|
||||
niriOverviewLastMode: { def: "apps" },
|
||||
|
||||
settingsSidebarExpandedIds: { def: "," },
|
||||
settingsSidebarCollapsedIds: { def: "," }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -11,6 +11,7 @@ var SPEC = {
|
||||
customThemeFile: { def: "" },
|
||||
registryThemeVariants: { def: {} },
|
||||
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
||||
matugenContrast: { def: 0, onChange: "regenSystemThemes" },
|
||||
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
||||
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
||||
|
||||
@@ -33,6 +34,7 @@ var SPEC = {
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
|
||||
firstDayOfWeek: { def: -1 },
|
||||
showWeekNumber: { def: false },
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
@@ -56,6 +58,10 @@ var SPEC = {
|
||||
modalElevationEnabled: { def: true },
|
||||
popoutElevationEnabled: { def: true },
|
||||
barElevationEnabled: { def: true },
|
||||
blurEnabled: { def: false },
|
||||
blurBorderColor: { def: "outline" },
|
||||
blurBorderCustomColor: { def: "#ffffff" },
|
||||
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||
wallpaperFillMode: { def: "Fill" },
|
||||
blurredWallpaperLayer: { def: false },
|
||||
blurWallpaperOnOverview: { def: false },
|
||||
@@ -167,8 +173,8 @@ var SPEC = {
|
||||
lockDateFormat: { def: "" },
|
||||
greeterRememberLastSession: { def: true },
|
||||
greeterRememberLastUser: { def: true },
|
||||
greeterEnableFprint: { def: false },
|
||||
greeterEnableU2f: { def: false },
|
||||
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||
greeterWallpaperPath: { def: "" },
|
||||
greeterUse24HourClock: { def: true },
|
||||
greeterShowSeconds: { def: false },
|
||||
@@ -187,6 +193,7 @@ var SPEC = {
|
||||
sortAppsAlphabetically: { def: false },
|
||||
appLauncherGridColumns: { def: 4 },
|
||||
spotlightCloseNiriOverview: { def: true },
|
||||
rememberLastQuery: { def: false },
|
||||
spotlightSectionViewModes: { def: {} },
|
||||
appDrawerSectionViewModes: { def: {} },
|
||||
niriOverviewOverlayEnabled: { def: true },
|
||||
@@ -268,6 +275,11 @@ var SPEC = {
|
||||
syncModeWithPortal: { def: true },
|
||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||
|
||||
muxType: { def: "tmux" },
|
||||
muxUseCustomCommand: { def: false },
|
||||
muxCustomCommand: { def: "" },
|
||||
muxSessionFilter: { def: "" },
|
||||
|
||||
runDmsMatugenTemplates: { def: true },
|
||||
matugenTemplateGtk: { def: true },
|
||||
matugenTemplateNiri: { def: true },
|
||||
@@ -293,11 +305,12 @@ var SPEC = {
|
||||
matugenTemplateZed: { def: true },
|
||||
|
||||
matugenTemplateNeovimSettings: {
|
||||
def: {
|
||||
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
||||
light: { baseTheme: "github_light", harmony: 0.5 }
|
||||
}
|
||||
def: {
|
||||
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
||||
light: { baseTheme: "github_light", harmony: 0.5 }
|
||||
}
|
||||
},
|
||||
matugenTemplateNeovimSetBackground: { def: true },
|
||||
|
||||
showDock: { def: false },
|
||||
dockAutoHide: { def: false },
|
||||
@@ -345,7 +358,7 @@ var SPEC = {
|
||||
lockScreenShowMediaPlayer: { def: true },
|
||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||
lockAtStartup: { def: false },
|
||||
enableFprint: { def: false },
|
||||
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
lockFingerprintCanEnable: { def: false, persist: false },
|
||||
@@ -355,7 +368,7 @@ var SPEC = {
|
||||
greeterFingerprintReady: { def: false, persist: false },
|
||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||
greeterFingerprintSource: { def: "none", persist: false },
|
||||
enableU2f: { def: false },
|
||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||
u2fMode: { def: "or" },
|
||||
u2fAvailable: { def: false, persist: false },
|
||||
lockU2fCanEnable: { def: false, persist: false },
|
||||
@@ -534,7 +547,17 @@ var SPEC = {
|
||||
clipboardEnterToPaste: { def: false },
|
||||
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] }
|
||||
launcherPluginOrder: { def: [] },
|
||||
|
||||
frameEnabled: { def: false },
|
||||
frameThickness: { def: 16 },
|
||||
frameRounding: { def: 23 },
|
||||
frameColor: { def: "" },
|
||||
frameOpacity: { def: 1.0 },
|
||||
frameScreenPreferences: { def: ["all"] },
|
||||
frameBarSize: { def: 40 },
|
||||
frameShowOnOverview: { def: false },
|
||||
frameBlurEnabled: { def: true }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 6;
|
||||
}
|
||||
|
||||
if (currentVersion < 11) {
|
||||
settings.configVersion = 11;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Frame
|
||||
import qs.Modules.WorkspaceOverlays
|
||||
import qs.Services
|
||||
|
||||
@@ -176,6 +177,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Frame {}
|
||||
|
||||
Repeater {
|
||||
id: dankBarRepeater
|
||||
model: ScriptModel {
|
||||
@@ -619,6 +622,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MuxModal {
|
||||
id: muxModal
|
||||
}
|
||||
|
||||
ClipboardHistoryModal {
|
||||
id: clipboardHistoryModalPopup
|
||||
|
||||
@@ -815,9 +822,8 @@ Item {
|
||||
|
||||
content: Component {
|
||||
Notepad {
|
||||
onHideRequested: {
|
||||
notepadSlideout.hide();
|
||||
}
|
||||
slideout: notepadSlideout
|
||||
onHideRequested: notepadSlideout.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ Rectangle {
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||
text: keyboardHints.enterToPaste
|
||||
? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
|
||||
: I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
@@ -3,6 +3,7 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -30,7 +31,7 @@ Item {
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
@@ -59,11 +60,25 @@ Item {
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||
if (focusedScreen) {
|
||||
if (screenChanged)
|
||||
contentWindow.visible = false;
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
if (!useSingleWindow) {
|
||||
if (screenChanged)
|
||||
clickCatcher.visible = false;
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
}
|
||||
if (screenChanged) {
|
||||
Qt.callLater(() => root._finishOpen());
|
||||
} else {
|
||||
_finishOpen();
|
||||
}
|
||||
}
|
||||
|
||||
function _finishOpen() {
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
@@ -215,6 +230,16 @@ Item {
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
||||
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
WlrLayershell.layer: {
|
||||
if (root.useOverlayLayer)
|
||||
@@ -393,6 +418,15 @@ Item {
|
||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: root.shouldBeVisible
|
||||
|
||||
312
quickshell/Modals/Common/InputModal.qml
Normal file
312
quickshell/Modals/Common/InputModal.qml
Normal file
@@ -0,0 +1,312 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:input-modal"
|
||||
keepPopoutsOpen: true
|
||||
|
||||
property string inputTitle: ""
|
||||
property string inputMessage: ""
|
||||
property string inputPlaceholder: ""
|
||||
property string inputText: ""
|
||||
property string confirmButtonText: "Confirm"
|
||||
property string cancelButtonText: "Cancel"
|
||||
property color confirmButtonColor: Theme.primary
|
||||
property var onConfirm: function (text) {}
|
||||
property var onCancel: function () {}
|
||||
property int selectedButton: -1
|
||||
property bool keyboardNavigation: false
|
||||
|
||||
function show(title, message, onConfirmCallback, onCancelCallback) {
|
||||
inputTitle = title || "";
|
||||
inputMessage = message || "";
|
||||
inputPlaceholder = "";
|
||||
inputText = "";
|
||||
confirmButtonText = "Confirm";
|
||||
cancelButtonText = "Cancel";
|
||||
confirmButtonColor = Theme.primary;
|
||||
onConfirm = onConfirmCallback || ((text) => {});
|
||||
onCancel = onCancelCallback || (() => {});
|
||||
selectedButton = -1;
|
||||
keyboardNavigation = false;
|
||||
open();
|
||||
}
|
||||
|
||||
function showWithOptions(options) {
|
||||
inputTitle = options.title || "";
|
||||
inputMessage = options.message || "";
|
||||
inputPlaceholder = options.placeholder || "";
|
||||
inputText = options.initialText || "";
|
||||
confirmButtonText = options.confirmText || "Confirm";
|
||||
cancelButtonText = options.cancelText || "Cancel";
|
||||
confirmButtonColor = options.confirmColor || Theme.primary;
|
||||
onConfirm = options.onConfirm || ((text) => {});
|
||||
onCancel = options.onCancel || (() => {});
|
||||
selectedButton = -1;
|
||||
keyboardNavigation = false;
|
||||
open();
|
||||
}
|
||||
|
||||
function confirmAndClose() {
|
||||
const text = inputText;
|
||||
close();
|
||||
if (onConfirm) {
|
||||
onConfirm(text);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAndClose() {
|
||||
close();
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
function selectButton() {
|
||||
if (selectedButton === 0) {
|
||||
cancelAndClose();
|
||||
} else {
|
||||
confirmAndClose();
|
||||
}
|
||||
}
|
||||
|
||||
shouldBeVisible: false
|
||||
allowStacking: true
|
||||
modalWidth: 350
|
||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
|
||||
enableShadow: true
|
||||
shouldHaveFocus: true
|
||||
onBackgroundClicked: cancelAndClose()
|
||||
onOpened: {
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.textInputRef) {
|
||||
contentLoader.item.textInputRef.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
content: Component {
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
implicitHeight: mainColumn.implicitHeight
|
||||
focus: true
|
||||
|
||||
property alias textInputRef: textInput
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
const textFieldFocused = textInput.activeFocus;
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
root.cancelAndClose();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Tab:
|
||||
if (textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 0;
|
||||
textInput.focus = false;
|
||||
} else {
|
||||
root.keyboardNavigation = true;
|
||||
if (root.selectedButton === -1) {
|
||||
root.selectedButton = 0;
|
||||
} else if (root.selectedButton === 0) {
|
||||
root.selectedButton = 1;
|
||||
} else {
|
||||
root.selectedButton = -1;
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (!textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 0;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (!textFieldFocused) {
|
||||
root.keyboardNavigation = true;
|
||||
root.selectedButton = 1;
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
if (root.selectedButton !== -1) {
|
||||
root.selectButton();
|
||||
} else {
|
||||
root.confirmAndClose();
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingL
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: root.inputTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.inputMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
visible: root.inputMessage !== ""
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: root.inputMessage !== "" ? Theme.spacingL : 0
|
||||
visible: root.inputMessage !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariantAlpha
|
||||
border.color: textInput.activeFocus ? Theme.primary : "transparent"
|
||||
border.width: textInput.activeFocus ? 1 : 0
|
||||
|
||||
TextInput {
|
||||
id: textInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
selectionColor: Theme.primary
|
||||
selectedTextColor: Theme.primaryText
|
||||
clip: true
|
||||
text: root.inputText
|
||||
onTextChanged: root.inputText = text
|
||||
|
||||
StyledText {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
text: root.inputPlaceholder
|
||||
visible: textInput.text === "" && !textInput.activeFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL * 1.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (root.keyboardNavigation && root.selectedButton === 0) {
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
} else if (cancelButton.containsMouse) {
|
||||
return Theme.surfacePressed;
|
||||
} else {
|
||||
return Theme.surfaceVariantAlpha;
|
||||
}
|
||||
}
|
||||
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
|
||||
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
text: root.cancelButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.cancelAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
const baseColor = root.confirmButtonColor;
|
||||
if (root.keyboardNavigation && root.selectedButton === 1) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
|
||||
} else if (confirmButton.containsMouse) {
|
||||
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
|
||||
} else {
|
||||
return baseColor;
|
||||
}
|
||||
}
|
||||
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
|
||||
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
|
||||
|
||||
StyledText {
|
||||
text: root.confirmButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.confirmAndClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ DankModal {
|
||||
|
||||
modalWidth: 680
|
||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
||||
backgroundColor: Theme.surfaceContainer
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
@@ -147,6 +147,13 @@ DankModal {
|
||||
return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function openColor(color: string): string {
|
||||
root.selectedColor = Qt.color(color);
|
||||
root.currentColor = Qt.color(color);
|
||||
root.updateFromColor(Qt.color(color));
|
||||
return open();
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
root.hide();
|
||||
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
||||
|
||||
@@ -207,9 +207,12 @@ Rectangle {
|
||||
selectedActionIndex = 0;
|
||||
}
|
||||
|
||||
function cycleAction() {
|
||||
function cycleAction(reverse = false) {
|
||||
if (actions.length > 0) {
|
||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||
if (! reverse)
|
||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||
else
|
||||
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
|
||||
ensureSelectedVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,14 @@ Item {
|
||||
signal itemExecuted
|
||||
signal searchCompleted
|
||||
signal modeChanged(string mode)
|
||||
signal queryChanged(string query)
|
||||
signal viewModeChanged(string sectionId, string mode)
|
||||
signal searchQueryRequested(string query)
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
SessionData.addLauncherHistory(searchQuery);
|
||||
|
||||
sections = [];
|
||||
flatModel = [];
|
||||
selectedItem = null;
|
||||
@@ -175,6 +178,33 @@ Item {
|
||||
}
|
||||
]
|
||||
|
||||
property int historyIndex: -1
|
||||
property string typingBackup: ""
|
||||
|
||||
function navigateHistory(direction) {
|
||||
let history = SessionData.launcherQueryHistory;
|
||||
if (history.length === 0)
|
||||
return;
|
||||
|
||||
if (historyIndex === -1)
|
||||
typingBackup = searchQuery;
|
||||
|
||||
let nextIndex = historyIndex + (direction === "up" ? 1 : -1);
|
||||
if (nextIndex >= history.length)
|
||||
nextIndex = history.length - 1;
|
||||
if (nextIndex < -1)
|
||||
nextIndex = -1;
|
||||
|
||||
if (nextIndex === historyIndex)
|
||||
return;
|
||||
historyIndex = nextIndex;
|
||||
|
||||
let targetText = (historyIndex === -1) ? typingBackup : history[historyIndex];
|
||||
|
||||
setSearchQuery(targetText);
|
||||
searchQueryRequested(targetText);
|
||||
}
|
||||
|
||||
property string fileSearchType: "all"
|
||||
property string fileSearchExt: ""
|
||||
property string fileSearchFolder: ""
|
||||
@@ -353,10 +383,13 @@ Item {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
function cycleMode() {
|
||||
function cycleMode(reverse = false) {
|
||||
var modes = ["all", "apps", "files", "plugins"];
|
||||
var currentIndex = modes.indexOf(searchMode);
|
||||
var nextIndex = (currentIndex + 1) % modes.length;
|
||||
if (!reverse)
|
||||
var nextIndex = (currentIndex + 1) % modes.length;
|
||||
else
|
||||
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
|
||||
setMode(modes[nextIndex]);
|
||||
}
|
||||
|
||||
@@ -493,6 +526,8 @@ Item {
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
queryChanged(searchQuery);
|
||||
|
||||
var currentVersion = _searchVersion;
|
||||
isSearching = true;
|
||||
var shouldResetSelection = _queryDrivenSearch;
|
||||
@@ -1651,6 +1686,9 @@ Item {
|
||||
function executeItem(item) {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
SessionData.addLauncherHistory(searchQuery);
|
||||
|
||||
if (item.type === "plugin_browse") {
|
||||
var browsePluginId = item.data?.pluginId;
|
||||
if (!browsePluginId)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -17,7 +17,6 @@ Item {
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _windowEnabled: true
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
@@ -99,8 +98,16 @@ Item {
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
|
||||
var targetQuery = "";
|
||||
|
||||
if (query) {
|
||||
targetQuery = query;
|
||||
} else if (SettingsData.rememberLastQuery) {
|
||||
targetQuery = SessionData.launcherLastQuery || "";
|
||||
}
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
spotlightContent.searchField.text = targetQuery;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
@@ -115,12 +122,10 @@ Item {
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
if (query) {
|
||||
spotlightContent.controller.setSearchQuery(query);
|
||||
} else {
|
||||
spotlightContent.controller.searchQuery = "";
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
spotlightContent.controller.historyIndex = -1;
|
||||
spotlightContent.controller.searchQuery = targetQuery;
|
||||
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
@@ -130,40 +135,47 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
closeCleanupTimer.stop();
|
||||
function _finishShow(query, mode) {
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", "");
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
}
|
||||
|
||||
function show() {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
_finishShow("", "");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow(query, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query, "");
|
||||
_finishShow(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -187,14 +199,20 @@ Item {
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", mode));
|
||||
return;
|
||||
}
|
||||
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
@@ -233,6 +251,7 @@ Item {
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
@@ -267,41 +286,39 @@ Item {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screen = launcherWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
if (!needsReset) {
|
||||
needsReset = true;
|
||||
const screenName = launcherWindow.screen?.name;
|
||||
if (screenName) {
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName) {
|
||||
needsReset = false;
|
||||
break;
|
||||
}
|
||||
if (Quickshell.screens[i].name === screenName)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsReset)
|
||||
return;
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (!newScreen)
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
launcherWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
||||
visible: spotlightOpen || isClosing
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scale)
|
||||
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
|
||||
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
|
||||
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
|
||||
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
@@ -435,6 +452,14 @@ Item {
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ FocusScope {
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editDgpuToggle.checked = existing?.launchOnDgpu || false;
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
@@ -64,6 +65,8 @@ FocusScope {
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
if (editDgpuToggle.checked)
|
||||
override.launchOnDgpu = true;
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
@@ -146,10 +149,18 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
controller.selectNext();
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("down");
|
||||
} else {
|
||||
controller.selectNext();
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
controller.selectPrevious();
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("up");
|
||||
} else {
|
||||
controller.selectPrevious();
|
||||
}
|
||||
return;
|
||||
case Qt.Key_PageDown:
|
||||
controller.selectPageDown(8);
|
||||
@@ -158,6 +169,10 @@ FocusScope {
|
||||
controller.selectPageUp(8);
|
||||
return;
|
||||
case Qt.Key_Right:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectRight();
|
||||
return;
|
||||
@@ -165,12 +180,25 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Left:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectLeft();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_H:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (hasCtrl) {
|
||||
controller.selectNext();
|
||||
@@ -185,6 +213,13 @@ FocusScope {
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_L:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_N:
|
||||
if (hasCtrl) {
|
||||
controller.selectNextSection();
|
||||
@@ -200,13 +235,19 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
if (actionPanel.hasActions) {
|
||||
if (hasCtrl && actionPanel.hasActions) {
|
||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
if (actionPanel.expanded)
|
||||
actionPanel.hide();
|
||||
if (hasCtrl && actionPanel.expanded) {
|
||||
const reverse = true;
|
||||
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectPrevious();
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
@@ -270,7 +311,7 @@ FocusScope {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !editMode
|
||||
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||
|
||||
Item {
|
||||
id: footerBar
|
||||
@@ -388,7 +429,7 @@ FocusScope {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
@@ -548,7 +589,6 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -697,8 +737,6 @@ FocusScope {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
anchors.fill: parent
|
||||
@@ -731,6 +769,7 @@ FocusScope {
|
||||
}
|
||||
function onSearchQueryRequested(query) {
|
||||
searchField.text = query;
|
||||
searchField.cursorPosition = query.length;
|
||||
}
|
||||
function onModeChanged() {
|
||||
extFilterField.text = "";
|
||||
@@ -941,6 +980,15 @@ FocusScope {
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: editDgpuToggle
|
||||
width: parent.width
|
||||
text: I18n.tr("Launch on dGPU by default")
|
||||
visible: SessionService.nvidiaCommand.length > 0
|
||||
checked: false
|
||||
onToggled: checked => editDgpuToggle.checked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -324,6 +324,8 @@ Item {
|
||||
height: 24
|
||||
z: 100
|
||||
visible: {
|
||||
if (BlurService.enabled)
|
||||
return false;
|
||||
if (mainListView.contentHeight <= mainListView.height)
|
||||
return false;
|
||||
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
||||
@@ -449,7 +451,7 @@ Item {
|
||||
case "apps":
|
||||
return "apps";
|
||||
default:
|
||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
||||
return "search_off";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,9 +487,9 @@ Item {
|
||||
case "plugins":
|
||||
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||
case "apps":
|
||||
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
||||
return I18n.tr("No apps found");
|
||||
default:
|
||||
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
||||
return I18n.tr("No results found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ function scoreItems(items, query, getFrecencyFn) {
|
||||
var item = items[i]
|
||||
var itemScore
|
||||
|
||||
if (query && item._preScored !== undefined) {
|
||||
if (item._preScored !== undefined && (query || item._preScored > 900)) {
|
||||
itemScore = item._preScored
|
||||
} else {
|
||||
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
||||
|
||||
@@ -75,6 +75,50 @@ StyledRect {
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||
property string videoThumbnailPath: {
|
||||
if (!delegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
|
||||
onVideoThumbnailPathChanged: {
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const size = _thumbnailPx;
|
||||
const fp = delegateRoot.filePath;
|
||||
Paths.mkdir(thumbDir);
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
@@ -124,7 +168,11 @@ StyledRect {
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? delegateRoot.filePath : "";
|
||||
if (!delegateRoot.fileIsDir && isImage)
|
||||
return delegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
@@ -149,7 +197,7 @@ StyledRect {
|
||||
source: gridPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: gridImageMask
|
||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir))
|
||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
@@ -175,7 +223,7 @@ StyledRect {
|
||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||
size: iconSizes[iconSizeIndex] * 0.45
|
||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode)
|
||||
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,46 @@ StyledRect {
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string videoThumbnailPath: {
|
||||
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
|
||||
onVideoThumbnailPathChanged: {
|
||||
_videoThumb = "";
|
||||
if (!videoThumbnailPath)
|
||||
return;
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const fp = listDelegateRoot.filePath;
|
||||
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
||||
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||
if (exitCode === 0) {
|
||||
_videoThumb = thumbPath;
|
||||
} else {
|
||||
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
@@ -127,7 +167,13 @@ StyledRect {
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? listDelegateRoot.filePath : ""
|
||||
property string imagePath: {
|
||||
if (!listDelegateRoot.fileIsDir && isImage)
|
||||
return listDelegateRoot.filePath;
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
@@ -141,7 +187,7 @@ StyledRect {
|
||||
source: listPreviewImage
|
||||
maskEnabled: true
|
||||
maskSource: listImageMask
|
||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
|
||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
@@ -166,7 +212,7 @@ StyledRect {
|
||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||
size: Theme.iconSize - 2
|
||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName)
|
||||
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ DankModal {
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: KeybindsService.cheatsheet.title || "Keybinds"
|
||||
text: KeybindsService.cheatsheet.title || i18n("Keybinds")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
@@ -309,10 +309,12 @@ DankModal {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
color: Theme.secondary
|
||||
text: modelData.key || ""
|
||||
text: (modelData.key || "").replace(/\+/g, " + ")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 148)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +327,7 @@ DankModal {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
633
quickshell/Modals/MuxModal.qml
Normal file
633
quickshell/Modals/MuxModal.qml
Normal file
@@ -0,0 +1,633 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankModal {
|
||||
id: muxModal
|
||||
|
||||
layerNamespace: "dms:mux"
|
||||
|
||||
property int selectedIndex: -1
|
||||
property string searchText: ""
|
||||
property var filteredSessions: []
|
||||
|
||||
function updateFilteredSessions() {
|
||||
var filtered = []
|
||||
var lowerSearch = searchText.trim().toLowerCase()
|
||||
for (var i = 0; i < MuxService.sessions.length; i++) {
|
||||
var session = MuxService.sessions[i]
|
||||
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
|
||||
continue
|
||||
filtered.push(session)
|
||||
}
|
||||
filteredSessions = filtered
|
||||
|
||||
if (selectedIndex >= filteredSessions.length) {
|
||||
selectedIndex = Math.max(0, filteredSessions.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
onSearchTextChanged: updateFilteredSessions()
|
||||
|
||||
Connections {
|
||||
target: MuxService
|
||||
function onSessionsChanged() {
|
||||
updateFilteredSessions()
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [muxModal.contentWindow]
|
||||
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide()
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
open()
|
||||
selectedIndex = -1
|
||||
searchText = ""
|
||||
MuxService.refreshSessions()
|
||||
shouldHaveFocus = true
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (muxPanel && muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
close()
|
||||
selectedIndex = -1
|
||||
searchText = ""
|
||||
}
|
||||
|
||||
function attachToSession(name) {
|
||||
MuxService.attachToSession(name)
|
||||
hide()
|
||||
}
|
||||
|
||||
function renameSession(name) {
|
||||
inputModal.showWithOptions({
|
||||
title: I18n.tr("Rename Session"),
|
||||
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
|
||||
initialText: name,
|
||||
onConfirm: function (newName) {
|
||||
MuxService.renameSession(name, newName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function killSession(name) {
|
||||
confirmModal.showWithOptions({
|
||||
title: I18n.tr("Kill Session"),
|
||||
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
|
||||
confirmText: I18n.tr("Kill"),
|
||||
confirmColor: Theme.primary,
|
||||
onConfirm: function () {
|
||||
MuxService.killSession(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createNewSession() {
|
||||
inputModal.showWithOptions({
|
||||
title: I18n.tr("New Session"),
|
||||
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
|
||||
onConfirm: function (name) {
|
||||
MuxService.createSession(name)
|
||||
hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
selectedIndex = Math.max(selectedIndex - 1, -1)
|
||||
}
|
||||
|
||||
function activateSelected() {
|
||||
if (selectedIndex === -1) {
|
||||
createNewSession()
|
||||
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
attachToSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
}
|
||||
|
||||
visible: false
|
||||
modalWidth: 600
|
||||
modalHeight: 600
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
cornerRadius: Theme.cornerRadius
|
||||
borderColor: Theme.outlineMedium
|
||||
borderWidth: 1
|
||||
enableShadow: true
|
||||
keepContentLoaded: true
|
||||
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
Timer {
|
||||
interval: 3000
|
||||
running: muxModal.shouldBeVisible
|
||||
repeat: true
|
||||
onTriggered: MuxService.refreshSessions()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
muxModal.show()
|
||||
return "MUX_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
muxModal.hide()
|
||||
return "MUX_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
muxModal.toggle()
|
||||
return "MUX_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "mux"
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
muxModal.show()
|
||||
return "TMUX_OPEN_SUCCESS"
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
muxModal.hide()
|
||||
return "TMUX_CLOSE_SUCCESS"
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
muxModal.toggle()
|
||||
return "TMUX_TOGGLE_SUCCESS"
|
||||
}
|
||||
|
||||
target: "tmux"
|
||||
}
|
||||
|
||||
InputModal {
|
||||
id: inputModal
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
muxModal.shouldHaveFocus = false;
|
||||
muxModal.contentWindow.visible = false;
|
||||
return;
|
||||
}
|
||||
if (muxModal.shouldBeVisible) {
|
||||
muxModal.contentWindow.visible = true;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (!muxModal.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
muxModal.shouldHaveFocus = true;
|
||||
muxModal.modalFocusScope.forceActiveFocus();
|
||||
if (muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: confirmModal
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
muxModal.shouldHaveFocus = false;
|
||||
muxModal.contentWindow.visible = false;
|
||||
return;
|
||||
}
|
||||
if (muxModal.shouldBeVisible) {
|
||||
muxModal.contentWindow.visible = true;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (!muxModal.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
muxModal.shouldHaveFocus = true;
|
||||
muxModal.modalFocusScope.forceActiveFocus();
|
||||
if (muxPanel.searchField) {
|
||||
muxPanel.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
directContent: Item {
|
||||
id: muxPanel
|
||||
|
||||
clip: false
|
||||
|
||||
property alias searchField: searchField
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
|
||||
(event.key === Qt.Key_Down)) {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
|
||||
(event.key === Qt.Key_Up)) {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
|
||||
createNewSession()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
|
||||
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
renameSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
|
||||
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
||||
killSession(filteredSessions[selectedIndex].name)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
hide()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
activateSelected()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: parent.height - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Header
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
const total = MuxService.sessions.length;
|
||||
const filtered = muxModal.filteredSessions.length;
|
||||
const activePart = total === 1
|
||||
? I18n.tr("%1 active session").arg(total)
|
||||
: I18n.tr("%1 active sessions").arg(total);
|
||||
const filteredPart = filtered === 1
|
||||
? I18n.tr("%1 filtered").arg(filtered)
|
||||
: I18n.tr("%1 filtered").arg(filtered);
|
||||
return activePart + ", " + filteredPart;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
// Search field
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: Theme.surfaceContainerHigh
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
leftIconName: "search"
|
||||
leftIconSize: Theme.iconSize
|
||||
leftIconColor: Theme.surfaceVariantText
|
||||
leftIconFocusedColor: Theme.primary
|
||||
showClearButton: true
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
placeholderText: I18n.tr("Search sessions...")
|
||||
keyForwardTargets: [muxPanel]
|
||||
|
||||
onTextEdited: {
|
||||
muxModal.searchText = text
|
||||
muxModal.selectedIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
// New Session Button
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 56
|
||||
radius: Theme.cornerRadius
|
||||
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
|
||||
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: 20
|
||||
color: Theme.primaryContainer
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "add"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("New Session")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: newMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.createNewSession()
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions List
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: muxModal.filteredSessions
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: parent.width
|
||||
height: 64
|
||||
radius: Theme.cornerRadius
|
||||
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
|
||||
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
|
||||
|
||||
MouseArea {
|
||||
id: sessionMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.attachToSession(modelData.name)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Avatar
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 40
|
||||
Layout.preferredHeight: 40
|
||||
radius: 20
|
||||
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name.charAt(0).toUpperCase()
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: modelData.attached ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
// Info
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
var parts = []
|
||||
if (modelData.windows !== "N/A")
|
||||
parts.push(modelData.windows === 1
|
||||
? I18n.tr("%1 window").arg(modelData.windows)
|
||||
: I18n.tr("%1 windows").arg(modelData.windows))
|
||||
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
|
||||
return parts.join(" \u2022 ")
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
// Rename button (tmux only)
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
radius: 18
|
||||
visible: MuxService.supportsRename
|
||||
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "edit"
|
||||
size: Theme.iconSizeSmall
|
||||
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: renameMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: muxModal.renameSession(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
radius: 18
|
||||
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "delete"
|
||||
size: Theme.iconSizeSmall
|
||||
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
muxModal.killSession(modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
Item {
|
||||
width: parent.width
|
||||
height: muxModal.filteredSessions.length === 0 ? 200 : 0
|
||||
visible: muxModal.filteredSessions.length === 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
|
||||
size: 48
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcuts bar
|
||||
Row {
|
||||
id: shortcutsBar
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
bottomPadding: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
var shortcuts = [
|
||||
{ key: "↑↓", label: I18n.tr("Navigate") },
|
||||
{ key: "↵", label: I18n.tr("Attach") },
|
||||
{ key: "^N", label: I18n.tr("New") },
|
||||
{ key: "^D", label: I18n.tr("Kill") },
|
||||
{ key: "Esc", label: I18n.tr("Close") }
|
||||
]
|
||||
if (MuxService.supportsRename)
|
||||
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
|
||||
return shortcuts
|
||||
}
|
||||
|
||||
delegate: Row {
|
||||
required property var modelData
|
||||
spacing: 4
|
||||
|
||||
Rectangle {
|
||||
width: keyText.width + Theme.spacingS
|
||||
height: keyText.height + 4
|
||||
radius: 4
|
||||
color: Theme.surfaceContainerHighest
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
text: modelData.key
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.label
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `Details for "${networkSSID}"`
|
||||
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -102,7 +102,7 @@ DankModal {
|
||||
id: detailsText
|
||||
|
||||
width: parent.width
|
||||
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -66,7 +66,7 @@ DankModal {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: `Details for "${networkID}"`
|
||||
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
@@ -102,7 +102,7 @@ DankModal {
|
||||
id: detailsText
|
||||
|
||||
width: parent.width
|
||||
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || "No information available"
|
||||
text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
@@ -503,5 +503,35 @@ FocusScope {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: muxLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 32
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: MuxTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: frameLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 33
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: FrameTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ Rectangle {
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 22
|
||||
},
|
||||
{
|
||||
"id": "frame",
|
||||
"text": I18n.tr("Frame"),
|
||||
"icon": "frame_source",
|
||||
"tabIndex": 33
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -156,7 +162,7 @@ Rectangle {
|
||||
{
|
||||
"id": "running_apps",
|
||||
"text": I18n.tr("Running Apps"),
|
||||
"icon": "apps",
|
||||
"icon": "app_registration",
|
||||
"tabIndex": 19,
|
||||
"hyprlandNiriOnly": true
|
||||
},
|
||||
@@ -237,7 +243,7 @@ Rectangle {
|
||||
{
|
||||
"id": "system",
|
||||
"text": I18n.tr("System"),
|
||||
"icon": "computer",
|
||||
"icon": "memory",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
@@ -266,6 +272,12 @@ Rectangle {
|
||||
"tabIndex": 8,
|
||||
"cupsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "multiplexers",
|
||||
"text": I18n.tr("Multiplexers"),
|
||||
"icon": "terminal",
|
||||
"tabIndex": 32
|
||||
},
|
||||
{
|
||||
"id": "window_rules",
|
||||
"text": I18n.tr("Window Rules"),
|
||||
@@ -364,6 +376,7 @@ Rectangle {
|
||||
if (_collapsedIds.indexOf(marker) < 0)
|
||||
_collapsedIds = _collapsedIds + id + ",";
|
||||
}
|
||||
SessionData.setSettingsSidebarState(_expandedIds, _collapsedIds);
|
||||
}
|
||||
|
||||
function _setAutoExpanded(id, value) {
|
||||
@@ -532,6 +545,11 @@ Rectangle {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Component.onCompleted: {
|
||||
root._expandedIds = SessionData.settingsSidebarExpandedIds;
|
||||
root._collapsedIds = SessionData.settingsSidebarCollapsedIds;
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: __m1
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
|
||||
@@ -90,7 +90,7 @@ DankPopout {
|
||||
if (!lc)
|
||||
return;
|
||||
|
||||
const query = _pendingQuery;
|
||||
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
|
||||
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
||||
_pendingMode = "";
|
||||
_pendingQuery = "";
|
||||
@@ -102,12 +102,9 @@ DankPopout {
|
||||
if (lc.controller) {
|
||||
lc.controller.searchMode = mode;
|
||||
lc.controller.pluginFilter = "";
|
||||
lc.controller.searchQuery = "";
|
||||
if (query) {
|
||||
lc.controller.setSearchQuery(query);
|
||||
} else {
|
||||
lc.controller.performSearch();
|
||||
}
|
||||
lc.controller.searchQuery = query;
|
||||
|
||||
lc.controller.performSearch();
|
||||
}
|
||||
lc.resetScroll?.();
|
||||
lc.actionPanel?.hide();
|
||||
@@ -136,7 +133,7 @@ DankPopout {
|
||||
QtObject {
|
||||
id: modalAdapter
|
||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||
property bool isClosing: false
|
||||
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||
|
||||
function hide() {
|
||||
appDrawerPopout.close();
|
||||
|
||||
@@ -86,7 +86,7 @@ Variants {
|
||||
|
||||
Component.onCompleted: {
|
||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@@ -100,10 +100,30 @@ Variants {
|
||||
Connections {
|
||||
target: currentWallpaper
|
||||
function onStatusChanged() {
|
||||
if (currentWallpaper.status === Image.Ready) {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||
return;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +187,8 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = nextWallpaper.source;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
@@ -175,6 +197,9 @@ Variants {
|
||||
return;
|
||||
}
|
||||
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
|
||||
nextWallpaper.source = newPath;
|
||||
|
||||
if (nextWallpaper.status === Image.Ready)
|
||||
@@ -201,6 +226,7 @@ Variants {
|
||||
visible: false
|
||||
opacity: 1
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
@@ -213,6 +239,7 @@ Variants {
|
||||
visible: false
|
||||
opacity: 0
|
||||
asynchronous: true
|
||||
retainWhileLoading: true
|
||||
smooth: true
|
||||
cache: true
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
@@ -295,6 +322,8 @@ Variants {
|
||||
root.useNextForEffect = false;
|
||||
nextWallpaper.source = "";
|
||||
root.transitionProgress = 0.0;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.effectActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ PluginComponent {
|
||||
id: detailRoot
|
||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
|
||||
DankActionButton {
|
||||
anchors.top: parent.top
|
||||
@@ -252,7 +252,7 @@ PluginComponent {
|
||||
width: parent ? parent.width : 300
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHighest
|
||||
color: Theme.surfaceLight
|
||||
border.width: 1
|
||||
border.color: Theme.outlineLight
|
||||
opacity: 1.0
|
||||
|
||||
@@ -33,7 +33,7 @@ Row {
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.surfaceContainer
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Theme.primarySelected
|
||||
border.width: 0
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -70,6 +70,16 @@ DankPopout {
|
||||
|
||||
backgroundInteractive: !anyModalOpen
|
||||
|
||||
onCredentialsPromptOpenChanged: {
|
||||
if (credentialsPromptOpen && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
|
||||
onPolkitModalOpenChanged: {
|
||||
if (polkitModalOpen && shouldBeVisible)
|
||||
close();
|
||||
}
|
||||
|
||||
customKeyboardFocus: {
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
|
||||
@@ -207,9 +207,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
|
||||
border.width: modelData === AudioService.source ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -218,9 +218,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||
border.width: modelData === AudioService.sink ? 2 : 1
|
||||
|
||||
DankRipple {
|
||||
id: deviceRipple
|
||||
@@ -397,9 +397,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
color: Theme.surfaceLight
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||
border.width: modelData === AudioService.sink ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -129,8 +129,9 @@ Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 64
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
color: Theme.surfaceLight
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
@@ -164,8 +165,9 @@ Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 64
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.width: 0
|
||||
color: Theme.surfaceLight
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -153,7 +153,7 @@ Item {
|
||||
width: 320
|
||||
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
opacity: modalVisible ? 1 : 0
|
||||
|
||||
@@ -229,7 +229,6 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!isConnected)
|
||||
@@ -243,8 +242,8 @@ Rectangle {
|
||||
if (isConnecting)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
if (deviceMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
|
||||
return Theme.primaryHoverLight;
|
||||
return Theme.surfaceLight;
|
||||
}
|
||||
|
||||
border.color: {
|
||||
@@ -252,8 +251,9 @@ Rectangle {
|
||||
return Theme.warning;
|
||||
if (isConnected)
|
||||
return Theme.primary;
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12);
|
||||
return Theme.outlineLight;
|
||||
}
|
||||
border.width: (isConnecting || isConnected) ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
@@ -490,9 +490,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||
border.color: Theme.outlineLight
|
||||
border.width: 1
|
||||
opacity: isInteractive ? 1 : 0.6
|
||||
|
||||
Row {
|
||||
|
||||
@@ -79,9 +79,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: modelData.mount === currentMountPath ? 2 : 0
|
||||
color: Theme.surfaceLight
|
||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
|
||||
border.width: modelData.mount === currentMountPath ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -308,9 +308,9 @@ Rectangle {
|
||||
width: parent.width
|
||||
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: Theme.primary
|
||||
border.width: 0
|
||||
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||
border.width: isActive ? 2 : 1
|
||||
|
||||
Row {
|
||||
id: wiredContentRow
|
||||
@@ -565,9 +565,9 @@ Rectangle {
|
||||
width: wifiContent.width
|
||||
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 0
|
||||
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
|
||||
border.width: wifiDelegate.isConnected ? 2 : 1
|
||||
|
||||
Row {
|
||||
id: wifiContentRow
|
||||
|
||||
@@ -10,6 +10,8 @@ Item {
|
||||
required property var axis
|
||||
required property var barConfig
|
||||
|
||||
visible: !SettingsData.frameEnabled
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -37,6 +39,8 @@ Item {
|
||||
}
|
||||
|
||||
property real rt: {
|
||||
if (SettingsData.frameEnabled)
|
||||
return SettingsData.frameRounding;
|
||||
if (barConfig?.squareCorners ?? false)
|
||||
return 0;
|
||||
if (barWindow.hasMaximizedToplevel)
|
||||
@@ -255,11 +259,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} 0`;
|
||||
d += ` L ${w - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
|
||||
let d = `M ${crE} 0`;
|
||||
d += ` L ${w - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} ${h + r}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
||||
@@ -273,9 +278,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
||||
}
|
||||
d += ` L 0 ${cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
d += ` L 0 ${crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -285,11 +290,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} ${fullH}`;
|
||||
d += ` L ${w - cr} ${fullH}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
|
||||
let d = `M ${crE} ${fullH}`;
|
||||
d += ` L ${w - crE} ${fullH}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} 0`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
||||
@@ -303,9 +309,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
}
|
||||
d += ` L 0 ${fullH - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
|
||||
d += ` L 0 ${fullH - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -314,11 +320,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M 0 ${cr}`;
|
||||
d += ` L 0 ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
|
||||
let d = `M 0 ${crE}`;
|
||||
d += ` L 0 ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w + r} ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
||||
@@ -332,9 +339,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
||||
}
|
||||
d += ` L ${cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
d += ` L ${crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -344,11 +351,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${fullW} ${cr}`;
|
||||
d += ` L ${fullW} ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
|
||||
let d = `M ${fullW} ${crE}`;
|
||||
d += ` L ${fullW} ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L 0 ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
||||
@@ -362,9 +370,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
}
|
||||
d += ` L ${fullW - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
|
||||
d += ` L ${fullW - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ Item {
|
||||
property real barThickness: 48
|
||||
property real barSpacing: 4
|
||||
property var barConfig: null
|
||||
property var blurBarWindow: null
|
||||
property bool overrideAxisLayout: false
|
||||
property bool forceVerticalLayout: false
|
||||
|
||||
@@ -357,6 +358,7 @@ Item {
|
||||
barThickness: root.barThickness
|
||||
barSpacing: root.barSpacing
|
||||
barConfig: root.barConfig
|
||||
blurBarWindow: root.blurBarWindow
|
||||
isFirst: index === 0
|
||||
isLast: index === centerRepeater.count - 1
|
||||
sectionSpacing: parent.itemSpacing
|
||||
|
||||
@@ -14,6 +14,8 @@ Item {
|
||||
required property var rootWindow
|
||||
required property var barConfig
|
||||
|
||||
readonly property var blurBarWindow: barWindow
|
||||
|
||||
property var leftWidgetsModel
|
||||
property var centerWidgetsModel
|
||||
property var rightWidgetsModel
|
||||
@@ -21,6 +23,31 @@ Item {
|
||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||
|
||||
readonly property real _frameLeftInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentLeftBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameRightInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentRightBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameTopInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentTopBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameBottomInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentBottomBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
|
||||
property alias hLeftSection: hLeftSection
|
||||
property alias hCenterSection: hCenterSection
|
||||
property alias hRightSection: hRightSection
|
||||
@@ -29,10 +56,14 @@ Item {
|
||||
property alias vRightSection: vRightSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
|
||||
anchors.topMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameTopInset
|
||||
anchors.bottomMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameBottomInset
|
||||
clip: false
|
||||
|
||||
property int componentMapRevision: 0
|
||||
@@ -408,6 +439,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: hLeftSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
RightSection {
|
||||
id: hRightSection
|
||||
@@ -434,6 +471,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: hRightSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
id: hCenterSection
|
||||
@@ -460,6 +503,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: hCenterSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -493,6 +542,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: vLeftSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
CenterSection {
|
||||
id: vCenterSection
|
||||
@@ -520,6 +575,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: vCenterSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
RightSection {
|
||||
id: vRightSection
|
||||
@@ -548,6 +609,12 @@ Item {
|
||||
value: topBarContent.barConfig
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
Binding {
|
||||
target: vRightSection
|
||||
property: "blurBarWindow"
|
||||
value: topBarContent.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,122 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
property var blurRegion: null
|
||||
property var _blurWidgetItems: []
|
||||
|
||||
function registerBlurWidget(item) {
|
||||
if (_blurWidgetItems.indexOf(item) >= 0)
|
||||
return;
|
||||
_blurWidgetItems = _blurWidgetItems.concat([item]);
|
||||
_blurRebuildTimer.restart();
|
||||
}
|
||||
|
||||
function unregisterBlurWidget(item) {
|
||||
const idx = _blurWidgetItems.indexOf(item);
|
||||
if (idx < 0)
|
||||
return;
|
||||
const arr = _blurWidgetItems.slice();
|
||||
arr.splice(idx, 1);
|
||||
_blurWidgetItems = arr;
|
||||
_blurRebuildTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _blurRebuildTimer
|
||||
interval: 1
|
||||
onTriggered: barBlur.rebuild()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: barBlur
|
||||
visible: false
|
||||
|
||||
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
|
||||
|
||||
function rebuild() {
|
||||
teardown();
|
||||
if (!BlurService.enabled || !BlurService.available)
|
||||
return;
|
||||
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
||||
// (including the bar area). The bar must not set its own competing blur region
|
||||
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
||||
if (SettingsData.frameEnabled)
|
||||
return;
|
||||
|
||||
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||
const hasBar = barHasTransparency;
|
||||
if (!hasBar && widgets.length === 0)
|
||||
return;
|
||||
|
||||
const cr = Theme.cornerRadius;
|
||||
let qml = 'import QtQuick; import Quickshell; Region {';
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
|
||||
}
|
||||
qml += '}';
|
||||
|
||||
try {
|
||||
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
|
||||
|
||||
if (hasBar) {
|
||||
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
|
||||
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
|
||||
region.width = Qt.binding(() => barUnitInset.width);
|
||||
region.height = Qt.binding(() => barUnitInset.height);
|
||||
region.radius = Qt.binding(() => barBackground.rt);
|
||||
}
|
||||
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
region[`w${i}`] = widgets[i];
|
||||
}
|
||||
|
||||
barWindow.BackgroundEffect.blurRegion = region;
|
||||
barWindow.blurRegion = region;
|
||||
} catch (e) {
|
||||
console.warn("BarBlur: Failed to create blur region:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function teardown() {
|
||||
if (!barWindow.blurRegion)
|
||||
return;
|
||||
try {
|
||||
barWindow.BackgroundEffect.blurRegion = null;
|
||||
} catch (e) {}
|
||||
barWindow.blurRegion.destroy();
|
||||
barWindow.blurRegion = null;
|
||||
}
|
||||
|
||||
onBarHasTransparencyChanged: _blurRebuildTimer.restart()
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() {
|
||||
barBlur.rebuild();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onFrameEnabledChanged() { barBlur.rebuild(); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: topBarSlide
|
||||
function onXChanged() {
|
||||
if (barWindow.blurRegion)
|
||||
barWindow.blurRegion.changed();
|
||||
}
|
||||
function onYChanged() {
|
||||
if (barWindow.blurRegion)
|
||||
barWindow.blurRegion.changed();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: rebuild()
|
||||
Component.onDestruction: teardown()
|
||||
}
|
||||
|
||||
WlrLayershell.layer: dBarLayer
|
||||
WlrLayershell.namespace: "dms:bar"
|
||||
|
||||
@@ -132,7 +248,9 @@ PanelWindow {
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property string _barId: barConfig?.id ?? "default"
|
||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
readonly property color _bgColor: SettingsData.frameEnabled
|
||||
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
|
||||
function _updateBackgroundAlpha() {
|
||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||
@@ -157,6 +275,7 @@ PanelWindow {
|
||||
property string screenName: modelData.name
|
||||
|
||||
property bool hasMaximizedToplevel: false
|
||||
property bool hasFullscreenToplevel: false
|
||||
property bool shouldHideForWindows: false
|
||||
|
||||
function _updateHasMaximizedToplevel() {
|
||||
@@ -179,6 +298,25 @@ PanelWindow {
|
||||
hasMaximizedToplevel = false;
|
||||
}
|
||||
|
||||
function _updateHasFullscreenToplevel() {
|
||||
if (!CompositorService.isHyprland) {
|
||||
hasFullscreenToplevel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
if (filtered[i]?.fullscreen) {
|
||||
// On niri, fullscreen windows in inactive columns should not hide the bar
|
||||
if (CompositorService.isNiri && !filtered[i]?.activated)
|
||||
continue;
|
||||
hasFullscreenToplevel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
hasFullscreenToplevel = false;
|
||||
}
|
||||
|
||||
function _updateShouldHideForWindows() {
|
||||
if (!(barConfig?.showOnWindowsOpen ?? false)) {
|
||||
shouldHideForWindows = false;
|
||||
@@ -258,7 +396,7 @@ PanelWindow {
|
||||
shouldHideForWindows = filtered.length > 0;
|
||||
}
|
||||
|
||||
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
|
||||
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
|
||||
|
||||
Behavior on effectiveSpacing {
|
||||
enabled: barWindow.visible
|
||||
@@ -269,7 +407,12 @@ PanelWindow {
|
||||
}
|
||||
|
||||
readonly property int notificationCount: NotificationService.notifications.length
|
||||
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property real effectiveBarThickness: SettingsData.frameEnabled
|
||||
? SettingsData.frameBarSize
|
||||
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
|
||||
? SettingsData.frameShowOnOverview
|
||||
: (barConfig?.openOnOverview ?? false)
|
||||
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||
|
||||
readonly property bool hasAdjacentTopBar: {
|
||||
@@ -289,7 +432,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -312,7 +455,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -336,7 +479,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -360,7 +503,7 @@ PanelWindow {
|
||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||
if (!onThisScreen)
|
||||
return false;
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
||||
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
@@ -485,6 +628,7 @@ PanelWindow {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
barWindow._updateHasMaximizedToplevel();
|
||||
barWindow._updateHasFullscreenToplevel();
|
||||
barWindow._updateShouldHideForWindows();
|
||||
}
|
||||
}
|
||||
@@ -493,6 +637,7 @@ PanelWindow {
|
||||
target: NiriService
|
||||
function onAllWorkspacesChanged() {
|
||||
barWindow._updateHasMaximizedToplevel();
|
||||
barWindow._updateHasFullscreenToplevel();
|
||||
barWindow._updateShouldHideForWindows();
|
||||
}
|
||||
}
|
||||
@@ -523,7 +668,7 @@ PanelWindow {
|
||||
|
||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
||||
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||
|
||||
@@ -664,10 +809,13 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
if (barWindow.hasFullscreenToplevel)
|
||||
return false;
|
||||
|
||||
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
|
||||
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
|
||||
if (barWindow.shouldHideForWindows)
|
||||
@@ -686,6 +834,8 @@ PanelWindow {
|
||||
onHasActivePopoutChanged: evaluateReveal()
|
||||
|
||||
function updateActivePopoutState() {
|
||||
if (!barWindow.screen)
|
||||
return;
|
||||
const screenName = barWindow.screen.name;
|
||||
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
||||
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
||||
@@ -756,7 +906,7 @@ PanelWindow {
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||
|
||||
@@ -13,6 +13,7 @@ Item {
|
||||
property real barThickness: 48
|
||||
property real barSpacing: 4
|
||||
property var barConfig: null
|
||||
property var blurBarWindow: null
|
||||
property bool overrideAxisLayout: false
|
||||
property bool forceVerticalLayout: false
|
||||
|
||||
@@ -59,6 +60,7 @@ Item {
|
||||
barThickness: root.barThickness
|
||||
barSpacing: root.barSpacing
|
||||
barConfig: root.barConfig
|
||||
blurBarWindow: root.blurBarWindow
|
||||
isFirst: index === 0
|
||||
isLast: index === rowRepeater.count - 1
|
||||
sectionSpacing: parent.rowSpacing
|
||||
@@ -103,6 +105,7 @@ Item {
|
||||
barThickness: root.barThickness
|
||||
barSpacing: root.barSpacing
|
||||
barConfig: root.barConfig
|
||||
blurBarWindow: root.blurBarWindow
|
||||
isFirst: index === 0
|
||||
isLast: index === columnRepeater.count - 1
|
||||
sectionSpacing: parent.columnSpacing
|
||||
|
||||
@@ -22,12 +22,12 @@ DankPopout {
|
||||
|
||||
function setProfile(profile) {
|
||||
if (typeof PowerProfiles === "undefined") {
|
||||
ToastService.showError("power-profiles-daemon not available");
|
||||
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
|
||||
return;
|
||||
}
|
||||
PowerProfiles.profile = profile;
|
||||
if (PowerProfiles.profile !== profile) {
|
||||
ToastService.showError("Failed to set power profile");
|
||||
ToastService.showError(I18n.tr("Failed to set power profile"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,10 +173,10 @@ DankPopout {
|
||||
width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2
|
||||
readonly property string timeInfoText: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return "Power profile management available";
|
||||
return I18n.tr("Power profile management available");
|
||||
const time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown") {
|
||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
||||
return BatteryService.isCharging ? I18n.tr("Time until full: %1").arg(time) : I18n.tr("Time remaining: %1").arg(time);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -188,7 +188,7 @@ DankPopout {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
|
||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||
@@ -338,7 +338,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown"
|
||||
text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : I18n.tr("Unknown")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Bold
|
||||
@@ -393,7 +393,7 @@ DankPopout {
|
||||
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
|
||||
|
||||
StyledText {
|
||||
text: modelData.model || `Battery ${index + 1}`
|
||||
text: modelData.model || I18n.tr("Battery %1").arg(index + 1)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
|
||||
@@ -13,6 +13,7 @@ Item {
|
||||
property real barThickness: 48
|
||||
property real barSpacing: 4
|
||||
property var barConfig: null
|
||||
property var blurBarWindow: null
|
||||
property bool overrideAxisLayout: false
|
||||
property bool forceVerticalLayout: false
|
||||
|
||||
@@ -61,6 +62,7 @@ Item {
|
||||
barThickness: root.barThickness
|
||||
barSpacing: root.barSpacing
|
||||
barConfig: root.barConfig
|
||||
blurBarWindow: root.blurBarWindow
|
||||
isFirst: index === 0
|
||||
isLast: index === rowRepeater.count - 1
|
||||
sectionSpacing: parent.rowSpacing
|
||||
@@ -105,6 +107,7 @@ Item {
|
||||
barThickness: root.barThickness
|
||||
barSpacing: root.barSpacing
|
||||
barConfig: root.barConfig
|
||||
blurBarWindow: root.blurBarWindow
|
||||
isFirst: index === 0
|
||||
isLast: index === columnRepeater.count - 1
|
||||
sectionSpacing: parent.columnSpacing
|
||||
|
||||
@@ -16,6 +16,7 @@ Loader {
|
||||
property real barThickness: 48
|
||||
property real barSpacing: 4
|
||||
property var barConfig: null
|
||||
property var blurBarWindow: null
|
||||
property bool isFirst: false
|
||||
property bool isLast: false
|
||||
property real sectionSpacing: 0
|
||||
@@ -92,6 +93,14 @@ Loader {
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "blurBarWindow" in root.item
|
||||
property: "blurBarWindow"
|
||||
value: root.blurBarWindow
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "axis" in root.item
|
||||
|
||||
@@ -248,7 +248,7 @@ BasePill {
|
||||
let appId = Paths.moddedAppId(rawAppId);
|
||||
|
||||
let coreAppData = null;
|
||||
if (rawAppId === "org.quickshell") {
|
||||
if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") {
|
||||
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||
if (coreAppData) {
|
||||
appId = coreAppData.builtInPluginId;
|
||||
@@ -630,7 +630,7 @@ BasePill {
|
||||
if (appItem.isFocused && colorizeEnabled) {
|
||||
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
|
||||
border.width: dragHandler.dragging ? 2 : 0
|
||||
@@ -697,7 +697,7 @@ BasePill {
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !coreIcon.visible
|
||||
layer.enabled: appItem.appId === "org.quickshell"
|
||||
layer.enabled: appItem.appId === "org.quickshell" || appItem.appId === "com.danklinux.dms"
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -990,7 +990,7 @@ BasePill {
|
||||
break;
|
||||
}
|
||||
|
||||
const shouldHidePin = modelData.appId === "org.quickshell";
|
||||
const shouldHidePin = modelData.appId === "org.quickshell" || modelData.appId === "com.danklinux.dms";
|
||||
const moddedId = Paths.moddedAppId(modelData.appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ PanelWindow {
|
||||
property int margin: 10
|
||||
property bool hidePin: false
|
||||
property var desktopEntry: null
|
||||
property bool isDmsWindow: appData?.appId === "org.quickshell"
|
||||
property bool isDmsWindow: appData?.appId === "org.quickshell" || appData?.appId === "com.danklinux.dms"
|
||||
|
||||
property bool isVertical: false
|
||||
property string edge: "top"
|
||||
|
||||
@@ -3,6 +3,7 @@ import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
BasePill {
|
||||
@@ -93,6 +94,15 @@ BasePill {
|
||||
PanelWindow {
|
||||
id: contextMenuWindow
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contextMenuWindow
|
||||
blurX: menuContainer.x
|
||||
blurY: menuContainer.y
|
||||
blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
|
||||
blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:clipboard-context-menu"
|
||||
|
||||
property bool isVertical: false
|
||||
@@ -187,8 +197,8 @@ BasePill {
|
||||
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
|
||||
opacity: contextMenuWindow.visible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
@@ -224,7 +234,7 @@ BasePill {
|
||||
width: parent.width
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -264,7 +274,7 @@ BasePill {
|
||||
width: parent.width
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
@@ -38,12 +39,20 @@ BasePill {
|
||||
property var _vAudio: null
|
||||
property var _vBrightness: null
|
||||
property var _vMic: null
|
||||
property var _interactionDelegates: []
|
||||
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
|
||||
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
|
||||
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
|
||||
|
||||
onIsVerticalOrientationChanged: root.clearInteractionRefs()
|
||||
|
||||
onWheel: function (wheelEvent) {
|
||||
const delta = wheelEvent.angleDelta.y;
|
||||
if (delta === 0)
|
||||
return;
|
||||
|
||||
root.refreshInteractionRefs();
|
||||
|
||||
const rootX = wheelEvent.x - root.leftMargin;
|
||||
const rootY = wheelEvent.y - root.topMargin;
|
||||
|
||||
@@ -72,6 +81,8 @@ BasePill {
|
||||
}
|
||||
|
||||
onRightClicked: function (rootX, rootY) {
|
||||
root.refreshInteractionRefs();
|
||||
|
||||
if (root.isVerticalOrientation && _vCol) {
|
||||
const pos = root.mapToItem(_vCol, rootX, rootY);
|
||||
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
||||
@@ -279,26 +290,142 @@ BasePill {
|
||||
return CupsService.getTotalJobsNum() > 0;
|
||||
}
|
||||
|
||||
function getControlCenterIconSize() {
|
||||
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
|
||||
}
|
||||
|
||||
function getEffectiveControlCenterGroupOrder() {
|
||||
const knownIds = root.defaultControlCenterGroupOrder;
|
||||
const savedOrder = root.widgetData?.controlCenterGroupOrder;
|
||||
const result = [];
|
||||
const seen = {};
|
||||
|
||||
if (savedOrder && typeof savedOrder.length === "number") {
|
||||
for (let i = 0; i < savedOrder.length; ++i) {
|
||||
const groupId = savedOrder[i];
|
||||
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
|
||||
continue;
|
||||
|
||||
seen[groupId] = true;
|
||||
result.push(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < knownIds.length; ++i) {
|
||||
const groupId = knownIds[i];
|
||||
if (seen[groupId])
|
||||
continue;
|
||||
|
||||
seen[groupId] = true;
|
||||
result.push(groupId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isGroupVisible(groupId) {
|
||||
switch (groupId) {
|
||||
case "screenSharing":
|
||||
return root.showScreenSharingIcon && NiriService.hasCasts;
|
||||
case "network":
|
||||
return root.showNetworkIcon && NetworkService.networkAvailable;
|
||||
case "vpn":
|
||||
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
|
||||
case "bluetooth":
|
||||
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
|
||||
case "audio":
|
||||
return root.showAudioIcon;
|
||||
case "microphone":
|
||||
return root.showMicIcon;
|
||||
case "brightness":
|
||||
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
|
||||
case "battery":
|
||||
return root.showBatteryIcon && BatteryService.batteryAvailable;
|
||||
case "printer":
|
||||
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isCompositeGroup(groupId) {
|
||||
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
|
||||
}
|
||||
|
||||
function getControlCenterRenderModel() {
|
||||
return root.effectiveControlCenterGroupOrder.map(groupId => ({
|
||||
"id": groupId,
|
||||
"visible": root.isGroupVisible(groupId),
|
||||
"composite": root.isCompositeGroup(groupId)
|
||||
}));
|
||||
}
|
||||
|
||||
function clearInteractionRefs() {
|
||||
root._hAudio = null;
|
||||
root._hBrightness = null;
|
||||
root._hMic = null;
|
||||
root._vAudio = null;
|
||||
root._vBrightness = null;
|
||||
root._vMic = null;
|
||||
}
|
||||
|
||||
function registerInteractionDelegate(isVertical, item) {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < root._interactionDelegates.length; ++i) {
|
||||
const entry = root._interactionDelegates[i];
|
||||
if (entry && entry.item === item) {
|
||||
entry.isVertical = isVertical;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
root._interactionDelegates = root._interactionDelegates.concat([
|
||||
{
|
||||
"isVertical": isVertical,
|
||||
"item": item
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
function unregisterInteractionDelegate(item) {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
|
||||
}
|
||||
|
||||
function refreshInteractionRefs() {
|
||||
root.clearInteractionRefs();
|
||||
|
||||
for (let i = 0; i < root._interactionDelegates.length; ++i) {
|
||||
const entry = root._interactionDelegates[i];
|
||||
const item = entry?.item;
|
||||
if (!item || !item.visible)
|
||||
continue;
|
||||
|
||||
const groupId = item.interactionGroupId;
|
||||
if (entry.isVertical) {
|
||||
if (groupId === "audio")
|
||||
root._vAudio = item;
|
||||
else if (groupId === "microphone")
|
||||
root._vMic = item;
|
||||
else if (groupId === "brightness")
|
||||
root._vBrightness = item;
|
||||
} else {
|
||||
if (groupId === "audio")
|
||||
root._hAudio = item;
|
||||
else if (groupId === "microphone")
|
||||
root._hMic = item;
|
||||
else if (groupId === "brightness")
|
||||
root._hBrightness = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasNoVisibleIcons() {
|
||||
if (root.showScreenSharingIcon && NiriService.hasCasts)
|
||||
return false;
|
||||
if (root.showNetworkIcon && NetworkService.networkAvailable)
|
||||
return false;
|
||||
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
|
||||
return false;
|
||||
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
|
||||
return false;
|
||||
if (root.showAudioIcon)
|
||||
return false;
|
||||
if (root.showMicIcon)
|
||||
return false;
|
||||
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
|
||||
return false;
|
||||
if (root.showBatteryIcon && BatteryService.batteryAvailable)
|
||||
return false;
|
||||
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
|
||||
return false;
|
||||
return true;
|
||||
return !root.controlCenterRenderModel.some(entry => entry.visible);
|
||||
}
|
||||
|
||||
content: Component {
|
||||
@@ -309,12 +436,7 @@ BasePill {
|
||||
Component.onCompleted: {
|
||||
root._hRow = controlIndicators;
|
||||
root._vCol = controlColumn;
|
||||
root._hAudio = audioIcon.parent;
|
||||
root._hBrightness = brightnessIcon.parent;
|
||||
root._hMic = micIcon.parent;
|
||||
root._vAudio = audioIconV.parent;
|
||||
root._vBrightness = brightnessIconV.parent;
|
||||
root._vMic = micIconV.parent;
|
||||
root.clearInteractionRefs();
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -324,162 +446,151 @@ BasePill {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
||||
Repeater {
|
||||
model: root.controlCenterRenderModel
|
||||
Item {
|
||||
id: verticalGroupItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
property string interactionGroupId: modelData.id
|
||||
|
||||
DankIcon {
|
||||
name: "screen_record"
|
||||
size: root.vIconSize
|
||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
width: parent.width
|
||||
height: {
|
||||
switch (modelData.id) {
|
||||
case "audio":
|
||||
return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
|
||||
case "microphone":
|
||||
return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0);
|
||||
case "brightness":
|
||||
return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
|
||||
default:
|
||||
return root.vIconSize;
|
||||
}
|
||||
}
|
||||
visible: modelData.visible
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
Component.onCompleted: {
|
||||
root.registerInteractionDelegate(true, verticalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
if (root) {
|
||||
root.unregisterInteractionDelegate(verticalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: root.refreshInteractionRefs()
|
||||
onInteractionGroupIdChanged: {
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: root.getNetworkIconName()
|
||||
size: root.vIconSize
|
||||
color: root.getNetworkIconColor()
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: !verticalGroupItem.modelData.composite
|
||||
name: {
|
||||
switch (verticalGroupItem.modelData.id) {
|
||||
case "screenSharing":
|
||||
return "screen_record";
|
||||
case "network":
|
||||
return root.getNetworkIconName();
|
||||
case "vpn":
|
||||
return "vpn_lock";
|
||||
case "bluetooth":
|
||||
return "bluetooth";
|
||||
case "battery":
|
||||
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
|
||||
case "printer":
|
||||
return "print";
|
||||
default:
|
||||
return "settings";
|
||||
}
|
||||
}
|
||||
size: root.vIconSize
|
||||
color: {
|
||||
switch (verticalGroupItem.modelData.id) {
|
||||
case "screenSharing":
|
||||
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
|
||||
case "network":
|
||||
return root.getNetworkIconColor();
|
||||
case "vpn":
|
||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||
case "bluetooth":
|
||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
||||
case "battery":
|
||||
return root.getBatteryIconColor();
|
||||
case "printer":
|
||||
return Theme.primary;
|
||||
default:
|
||||
return Theme.widgetIconColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
visible: verticalGroupItem.modelData.id === "audio"
|
||||
name: root.getVolumeIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "vpn_lock"
|
||||
size: root.vIconSize
|
||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
NumericText {
|
||||
id: audioPercentV
|
||||
visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: audioIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
DankIcon {
|
||||
id: micIconV
|
||||
visible: verticalGroupItem.modelData.id === "microphone"
|
||||
name: root.getMicIconName()
|
||||
size: root.vIconSize
|
||||
color: root.getMicIconColor()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "bluetooth"
|
||||
size: root.vIconSize
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
NumericText {
|
||||
id: micPercentV
|
||||
visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: micIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
|
||||
visible: root.showAudioIcon
|
||||
DankIcon {
|
||||
id: brightnessIconV
|
||||
visible: verticalGroupItem.modelData.id === "brightness"
|
||||
name: root.getBrightnessIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: audioIconV
|
||||
name: root.getVolumeIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: audioPercentV
|
||||
visible: root.showAudioPercent
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: audioIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
|
||||
visible: root.showMicIcon
|
||||
|
||||
DankIcon {
|
||||
id: micIconV
|
||||
name: root.getMicIconName()
|
||||
size: root.vIconSize
|
||||
color: root.getMicIconColor()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: micPercentV
|
||||
visible: root.showMicPercent
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: micIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
|
||||
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
|
||||
|
||||
DankIcon {
|
||||
id: brightnessIconV
|
||||
name: root.getBrightnessIconName()
|
||||
size: root.vIconSize
|
||||
color: Theme.widgetIconColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: brightnessPercentV
|
||||
visible: root.showBrightnessPercent
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: brightnessIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
||||
|
||||
DankIcon {
|
||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||
size: root.vIconSize
|
||||
color: root.getBatteryIconColor()
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: root.vIconSize
|
||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
||||
|
||||
DankIcon {
|
||||
name: "print"
|
||||
size: root.vIconSize
|
||||
color: Theme.primary
|
||||
anchors.centerIn: parent
|
||||
NumericText {
|
||||
id: brightnessPercentV
|
||||
visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: brightnessIconV.bottom
|
||||
anchors.topMargin: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,157 +614,206 @@ BasePill {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "screen_record"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
||||
}
|
||||
Repeater {
|
||||
model: root.controlCenterRenderModel
|
||||
|
||||
DankIcon {
|
||||
id: networkIcon
|
||||
name: root.getNetworkIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.getNetworkIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
||||
}
|
||||
Item {
|
||||
id: horizontalGroupItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
property string interactionGroupId: modelData.id
|
||||
|
||||
DankIcon {
|
||||
id: vpnIcon
|
||||
name: "vpn_lock"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
||||
}
|
||||
width: {
|
||||
switch (modelData.id) {
|
||||
case "audio":
|
||||
return audioGroup.width;
|
||||
case "microphone":
|
||||
return micGroup.width;
|
||||
case "brightness":
|
||||
return brightnessGroup.width;
|
||||
default:
|
||||
return root.getControlCenterIconSize();
|
||||
}
|
||||
}
|
||||
implicitWidth: width
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
visible: modelData.visible
|
||||
|
||||
DankIcon {
|
||||
id: bluetoothIcon
|
||||
name: "bluetooth"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.registerInteractionDelegate(false, horizontalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
if (root) {
|
||||
root.unregisterInteractionDelegate(horizontalGroupItem);
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
}
|
||||
onVisibleChanged: root.refreshInteractionRefs()
|
||||
onInteractionGroupIdChanged: {
|
||||
root.refreshInteractionRefs();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4
|
||||
implicitWidth: width
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showAudioIcon
|
||||
DankIcon {
|
||||
id: iconOnlyItem
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
visible: !horizontalGroupItem.modelData.composite
|
||||
name: {
|
||||
switch (horizontalGroupItem.modelData.id) {
|
||||
case "screenSharing":
|
||||
return "screen_record";
|
||||
case "network":
|
||||
return root.getNetworkIconName();
|
||||
case "vpn":
|
||||
return "vpn_lock";
|
||||
case "bluetooth":
|
||||
return "bluetooth";
|
||||
case "battery":
|
||||
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
|
||||
case "printer":
|
||||
return "print";
|
||||
default:
|
||||
return "settings";
|
||||
}
|
||||
}
|
||||
size: root.getControlCenterIconSize()
|
||||
color: {
|
||||
switch (horizontalGroupItem.modelData.id) {
|
||||
case "screenSharing":
|
||||
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
|
||||
case "network":
|
||||
return root.getNetworkIconColor();
|
||||
case "vpn":
|
||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||
case "bluetooth":
|
||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
||||
case "battery":
|
||||
return root.getBatteryIconColor();
|
||||
case "printer":
|
||||
return Theme.primary;
|
||||
default:
|
||||
return Theme.widgetIconColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
name: root.getVolumeIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
Rectangle {
|
||||
id: audioGroup
|
||||
width: audioContent.implicitWidth + 2
|
||||
implicitWidth: width
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: horizontalGroupItem.modelData.id === "audio"
|
||||
|
||||
Row {
|
||||
id: audioContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
DankIcon {
|
||||
id: audioIcon
|
||||
name: root.getVolumeIconName()
|
||||
size: root.getControlCenterIconSize()
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: audioPercent
|
||||
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: visible ? implicitWidth : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: micGroup
|
||||
width: micContent.implicitWidth + 2
|
||||
implicitWidth: width
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: horizontalGroupItem.modelData.id === "microphone"
|
||||
|
||||
Row {
|
||||
id: micContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
DankIcon {
|
||||
id: micIcon
|
||||
name: root.getMicIconName()
|
||||
size: root.getControlCenterIconSize()
|
||||
color: root.getMicIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: micPercent
|
||||
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: visible ? implicitWidth : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: brightnessGroup
|
||||
width: brightnessContent.implicitWidth + 2
|
||||
implicitWidth: width
|
||||
height: parent.height
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: horizontalGroupItem.modelData.id === "brightness"
|
||||
|
||||
Row {
|
||||
id: brightnessContent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
DankIcon {
|
||||
id: brightnessIcon
|
||||
name: root.getBrightnessIconName()
|
||||
size: root.getControlCenterIconSize()
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: brightnessPercent
|
||||
visible: root.showBrightnessPercent && isFinite(getBrightness())
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: visible ? implicitWidth : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: audioPercent
|
||||
visible: root.showAudioPercent
|
||||
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: audioIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.reservedWidth : 0) + 4
|
||||
implicitWidth: width
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showMicIcon
|
||||
|
||||
DankIcon {
|
||||
id: micIcon
|
||||
name: root.getMicIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.getMicIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: micPercent
|
||||
visible: root.showMicPercent
|
||||
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: micIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.reservedWidth : 0) + 4
|
||||
height: root.widgetThickness - root.horizontalPadding * 2
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
|
||||
|
||||
DankIcon {
|
||||
id: brightnessIcon
|
||||
name: root.getBrightnessIconName()
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: brightnessPercent
|
||||
visible: root.showBrightnessPercent
|
||||
text: Math.round(getBrightness() * 100) + "%"
|
||||
reserveText: "100%"
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: brightnessIcon.right
|
||||
anchors.leftMargin: 2
|
||||
width: reservedWidth
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: batteryIcon
|
||||
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: root.getBatteryIconColor()
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showBatteryIcon && BatteryService.batteryAvailable
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: printerIcon
|
||||
name: "print"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "settings"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
size: root.getControlCenterIconSize()
|
||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.hasNoVisibleIcons()
|
||||
|
||||
@@ -87,11 +87,11 @@ BasePill {
|
||||
}
|
||||
|
||||
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
|
||||
return workspaceWindows.length > 0 && activeWindow && activeWindow.title;
|
||||
return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId);
|
||||
}
|
||||
|
||||
if (CompositorService.isHyprland) {
|
||||
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
|
||||
if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
return activeWindow && activeWindow.title;
|
||||
return activeWindow && (activeWindow.title || activeWindow.appId);
|
||||
}
|
||||
|
||||
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
|
||||
@@ -145,7 +145,7 @@ BasePill {
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
layer.enabled: activeWindow && activeWindow.appId === "org.quickshell"
|
||||
layer.enabled: activeWindow && (activeWindow.appId === "org.quickshell" || activeWindow.appId === "com.danklinux.dms")
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -212,17 +212,19 @@ BasePill {
|
||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
||||
const appName = appText.text;
|
||||
|
||||
if (compactMode && title === appName) {
|
||||
if (compactMode) {
|
||||
if (!title || title === appName)
|
||||
return title || appName;
|
||||
if (title.endsWith(appName))
|
||||
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
|
||||
return title;
|
||||
}
|
||||
|
||||
if (!title || !appName) {
|
||||
if (!title || !appName)
|
||||
return title;
|
||||
}
|
||||
|
||||
if (title.endsWith(appName)) {
|
||||
if (title.endsWith(appName))
|
||||
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ BasePill {
|
||||
height: 20
|
||||
radius: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
visible: root.playerAvailable
|
||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
||||
|
||||
@@ -411,7 +411,7 @@ BasePill {
|
||||
height: 20
|
||||
radius: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
visible: playerAvailable
|
||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ BasePill {
|
||||
width: parent.width
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: tabArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
@@ -327,7 +327,7 @@ BasePill {
|
||||
width: parent.width
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: newNoteArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -273,7 +273,7 @@ BasePill {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
|
||||
// App icon
|
||||
@@ -296,7 +296,7 @@ BasePill {
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
layer.enabled: appId === "org.quickshell"
|
||||
layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms"
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -528,7 +528,7 @@ BasePill {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent";
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
|
||||
IconImage {
|
||||
@@ -550,7 +550,7 @@ BasePill {
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
layer.enabled: appId === "org.quickshell"
|
||||
layer.enabled: appId === "org.quickshell" || appId === "com.danklinux.dms"
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
@@ -738,6 +738,15 @@ BasePill {
|
||||
sourceComponent: PanelWindow {
|
||||
id: contextMenuWindow
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contextMenuWindow
|
||||
blurX: contextMenuRect.x
|
||||
blurY: contextMenuRect.y
|
||||
blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
|
||||
blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
property var currentWindow: null
|
||||
property bool isVisible: false
|
||||
property point anchorPos: Qt.point(0, 0)
|
||||
@@ -830,6 +839,7 @@ BasePill {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contextMenuRect
|
||||
x: {
|
||||
if (contextMenuWindow.isVertical) {
|
||||
if (contextMenuWindow.edge === "left") {
|
||||
@@ -858,13 +868,13 @@ BasePill {
|
||||
height: 32
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||
color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user