mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 15:29:43 -04:00
Compare commits
92 Commits
bb08e1233a
...
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 | ||
|
|
e7c8d208e2 | ||
|
|
0e2162cf29 | ||
|
|
4cf9b0adc7 | ||
|
|
1661d32641 | ||
|
|
aa59187403 |
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"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
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
|
done
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
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
|
# 1.4.0
|
||||||
|
|
||||||
- Overhauled system monitor, graphs, styling
|
- 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%)
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
--json - JSON with all formats
|
--json - JSON with all formats
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
dms color pick # Pick color, output as hex
|
dms color pick # Pick color, output as hex
|
||||||
dms color pick --rgb # Output as RGB
|
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("hsv", false, "Output as HSV (H S% V%)")
|
||||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
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("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().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
@@ -113,7 +117,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
fmt.Println(output)
|
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)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
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{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
PreRunE: findConfig,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -526,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
@@ -25,6 +26,11 @@ var greeterCmd = &cobra.Command{
|
|||||||
Long: "Manage DMS greeter (greetd)",
|
Long: "Manage DMS greeter (greetd)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
greeterConfigSyncFn = greeter.SyncDMSConfigs
|
||||||
|
sharedAuthSyncFn = sharedpam.SyncAuthConfig
|
||||||
|
)
|
||||||
|
|
||||||
var greeterInstallCmd = &cobra.Command{
|
var greeterInstallCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
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)")
|
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 {
|
func installGreeter(nonInteractive bool) error {
|
||||||
fmt.Println("=== DMS Greeter Installation ===")
|
fmt.Println("=== DMS Greeter Installation ===")
|
||||||
|
|
||||||
@@ -243,7 +259,9 @@ func installGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +296,7 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !nonInteractive {
|
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
|
var response string
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
if strings.ToLower(strings.TrimSpace(response)) != "y" {
|
||||||
@@ -297,8 +315,8 @@ func uninstallGreeter(nonInteractive bool) error {
|
|||||||
fmt.Println(" ✓ greetd disabled")
|
fmt.Println(" ✓ greetd disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nRemoving DMS PAM configuration...")
|
fmt.Println("\nRemoving DMS authentication configuration...")
|
||||||
if err := greeter.RemoveGreeterPamManagedBlock(logFunc, ""); err != nil {
|
if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil {
|
||||||
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
|
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 {
|
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||||
if !nonInteractive {
|
if !nonInteractive {
|
||||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
fmt.Println("=== DMS Greeter Sync ===")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +739,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,8 +756,9 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
|||||||
|
|
||||||
fmt.Println("\n=== Sync Complete ===")
|
fmt.Println("\n=== Sync Complete ===")
|
||||||
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
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 {
|
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.")
|
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) {
|
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
|
||||||
if pamText == "" {
|
return sharedpam.ParseManagedGreeterPamAuth(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageInstallHint() string {
|
func packageInstallHint() string {
|
||||||
@@ -1490,6 +1481,19 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
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 {
|
} else {
|
||||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||||
fmt.Printf(" %s\n", packageInstallHint())
|
fmt.Printf(" %s\n", packageInstallHint())
|
||||||
@@ -1497,6 +1501,20 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguration Symlinks:")
|
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 {
|
symlinks := []struct {
|
||||||
source string
|
source string
|
||||||
target string
|
target string
|
||||||
@@ -1513,9 +1531,9 @@ func checkGreeterStatus() error {
|
|||||||
desc: "Session state",
|
desc: "Session state",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
source: colorSyncInfo.SourcePath,
|
||||||
target: filepath.Join(cacheDir, "colors.json"),
|
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)
|
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:")
|
fmt.Println("\nGreeter Wallpaper Override:")
|
||||||
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
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)")
|
fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
|
||||||
}
|
}
|
||||||
if legacyManaged {
|
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
|
allGood = false
|
||||||
}
|
}
|
||||||
enableFprintToggle, enableU2fToggle := false, 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
|
enableFprintToggle = enableFprint
|
||||||
enableU2fToggle = enableU2f
|
enableU2fToggle = enableU2f
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr)
|
fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||||
includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
|
||||||
fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser()
|
fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser()
|
||||||
|
|
||||||
if managedFprint && includedFprintFile != "" {
|
if managedFprint && includedFprintFile != "" {
|
||||||
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", 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
|
allGood = false
|
||||||
}
|
}
|
||||||
if managedU2f && includedU2fFile != "" {
|
if managedU2f && includedU2fFile != "" {
|
||||||
fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", 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
|
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("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().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().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")
|
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")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -87,6 +89,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
Mode: matugen.ColorMode(mode),
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
|
Contrast: contrast,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
@@ -128,6 +131,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
|
"contrast": opts.Contrast,
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ var (
|
|||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
|
ssNoConfirm bool
|
||||||
|
ssReset bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,8 +52,10 @@ Examples:
|
|||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window (Hyprland)
|
||||||
dms screenshot last # Last region (pre-selected)
|
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-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
}
|
}
|
||||||
@@ -119,6 +123,8 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
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(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
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.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
@@ -142,6 +148,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
|
config.NoConfirm = ssNoConfirm
|
||||||
|
config.Reset = ssReset
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
|
authCmd.AddCommand(authSyncCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
|
authCmd.AddCommand(authSyncCmd)
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
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() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
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() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
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
|
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) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -627,10 +670,21 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
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...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
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)
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||||
|
|
||||||
if stdinSource, ok := data.(*os.File); ok {
|
stdout, err := cmd.StdoutPipe()
|
||||||
cmd.Stdin = stdinSource
|
|
||||||
return cmd.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
switch src := data.(type) {
|
||||||
return fmt.Errorf("start: %w", err)
|
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 {
|
var buf [1]byte
|
||||||
stdin.Close()
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
}
|
}
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
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 {
|
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
cachedData, err := createClipboardCacheFile()
|
cachedData, err := createClipboardCacheFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,6 +260,7 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
|
match app-id=r#"com.danklinux.dms$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -135,6 +135,42 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
||||||
|
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
||||||
|
data, err := os.ReadFile(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
var pkg string
|
||||||
|
var target *[]string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "makedepends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "makedepends = ")
|
||||||
|
target = &makedeps
|
||||||
|
case strings.HasPrefix(line, "depends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "depends = ")
|
||||||
|
target = &deps
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
||||||
|
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
||||||
|
pkg = pkg[:idx]
|
||||||
|
}
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg != "" {
|
||||||
|
*target = append(*target, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps, makedeps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||||
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -524,6 +560,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
||||||
|
if visited[pkg] {
|
||||||
|
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
visited[pkg] = true
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -610,39 +656,61 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Classifying dependencies as system or AUR",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies from .SRCINFO
|
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||||
depFilter := ""
|
if err != nil {
|
||||||
if pkg == "dms-shell-git" {
|
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||||
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
seen := make(map[string]bool)
|
||||||
fmt.Sprintf(`
|
var systemPkgs []string
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
var aurPkgs []string
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, depFilter, sudoPassword))
|
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
if seen[dep] || a.packageInstalled(dep) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dep] = true
|
||||||
|
if a.isInSystemRepo(dep) {
|
||||||
|
systemPkgs = append(systemPkgs, dep)
|
||||||
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
if len(systemPkgs) > 0 {
|
||||||
fmt.Sprintf(`
|
progressChan <- InstallProgressMsg{
|
||||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
Phase: PhaseAURPackages,
|
||||||
if [ ! -z "$makedeps" ]; then
|
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||||
fi
|
IsComplete: false,
|
||||||
`, srcinfoPath, sudoPassword))
|
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
for _, aurDep := range aurPkgs {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
||||||
|
}
|
||||||
|
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
||||||
|
startProgress+0.35*(endProgress-startProgress),
|
||||||
|
startProgress+0.39*(endProgress-startProgress),
|
||||||
|
visited,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -104,13 +103,15 @@ func debianPackageInstalledPrecisely(pkg string) bool {
|
|||||||
return strings.TrimSpace(string(output)) == "installed"
|
return strings.TrimSpace(string(output)) == "installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsString(values []string, target string) bool {
|
func debianRepoArchitecture(arch string) string {
|
||||||
for _, value := range values {
|
switch arch {
|
||||||
if value == target {
|
case "amd64", "x86_64":
|
||||||
return true
|
return "amd64"
|
||||||
}
|
case "arm64", "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return arch
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
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
|
// 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{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"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 {
|
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||||
for _, pkg := range extraPkgs {
|
for _, pkg := range extraPkgs {
|
||||||
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -14,6 +15,8 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"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/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/sblinch/kdl-go"
|
"github.com/sblinch/kdl-go"
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
@@ -23,26 +26,7 @@ var appArmorProfileData []byte
|
|||||||
|
|
||||||
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
||||||
|
|
||||||
const (
|
const GreeterCacheDir = "/var/cache/dms-greeter"
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectDMSPath() (string, error) {
|
func DetectDMSPath() (string, error) {
|
||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
@@ -628,7 +612,6 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
|||||||
runtimeDirs := []string{
|
runtimeDirs := []string{
|
||||||
filepath.Join(cacheDir, ".local"),
|
filepath.Join(cacheDir, ".local"),
|
||||||
filepath.Join(cacheDir, ".local", "state"),
|
filepath.Join(cacheDir, ".local", "state"),
|
||||||
filepath.Join(cacheDir, ".local", "state", "wireplumber"),
|
|
||||||
filepath.Join(cacheDir, ".local", "share"),
|
filepath.Join(cacheDir, ".local", "share"),
|
||||||
filepath.Join(cacheDir, ".cache"),
|
filepath.Join(cacheDir, ".cache"),
|
||||||
}
|
}
|
||||||
@@ -748,49 +731,6 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
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.
|
// 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.
|
// It is a no-op when AppArmor is not active or the profile does not exist.
|
||||||
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
|
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, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
||||||
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
{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, ".cache", "quickshell"), "quickshell cache"},
|
||||||
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
||||||
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
||||||
@@ -1109,7 +1050,218 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
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()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -1132,11 +1284,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
target: filepath.Join(cacheDir, "session.json"),
|
target: filepath.Join(cacheDir, "session.json"),
|
||||||
desc: "state (wallpaper configuration)",
|
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 {
|
for _, link := range symlinks {
|
||||||
@@ -1162,12 +1309,21 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syncGreeterWallpaperOverride(homeDir, cacheDir, logFunc, sudoPassword); err != nil {
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve greeter color source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := syncGreeterPamConfig(homeDir, logFunc, sudoPassword, forceAuth); err != nil {
|
if err := syncGreeterDynamicOverrideColors(dmsPath, homeDir, state, logFunc); err != nil {
|
||||||
return fmt.Errorf("greeter PAM config sync failed: %w", err)
|
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" {
|
if strings.ToLower(compositor) != "niri" {
|
||||||
@@ -1181,23 +1337,9 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string), sudoPassword string) error {
|
func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPassword string, state greeterThemeSyncState) 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)
|
|
||||||
}
|
|
||||||
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
if settings.GreeterWallpaperPath == "" {
|
if state.ResolvedGreeterWallpaperPath == "" {
|
||||||
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
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 {
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||||
}
|
}
|
||||||
src := settings.GreeterWallpaperPath
|
src := state.ResolvedGreeterWallpaperPath
|
||||||
if !filepath.IsAbs(src) {
|
|
||||||
src = filepath.Join(homeDir, src)
|
|
||||||
}
|
|
||||||
st, err := os.Stat(src)
|
st, err := os.Stat(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
|
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
|
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 {
|
type niriGreeterSync struct {
|
||||||
processed map[string]bool
|
processed map[string]bool
|
||||||
nodes []*document.Node
|
nodes []*document.Node
|
||||||
@@ -2280,10 +2047,15 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
logFunc("Synchronizing DMS configurations...")
|
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(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...")
|
logFunc("Checking for conflicting display managers...")
|
||||||
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: %v", err))
|
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
|
Mode ColorMode
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
|
Contrast float64
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -227,6 +229,7 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
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 := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -263,6 +266,7 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -274,6 +278,10 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -294,6 +302,13 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return true, nil
|
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 {
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
userConfigPath := filepath.Join(opts.ConfigDir, "matugen", "config.toml")
|
||||||
|
|
||||||
@@ -331,6 +346,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -597,10 +616,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
@@ -678,6 +697,7 @@ func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
|||||||
baseArgs = []string{opts.Kind, opts.Value}
|
baseArgs = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||||
if flags.isV4 {
|
if flags.isV4 {
|
||||||
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -59,7 +60,11 @@ func Send(n Notification) error {
|
|||||||
|
|
||||||
hints := map[string]dbus.Variant{}
|
hints := map[string]dbus.Variant{}
|
||||||
if n.FilePath != "" {
|
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)
|
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 {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
func getAllOutputInfos() map[string]*outputInfo {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
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) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*outputInfo, len(heads))
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == outputName {
|
if state.name == "" {
|
||||||
return &outputInfo{
|
continue
|
||||||
x: state.x,
|
}
|
||||||
y: state.y,
|
result[state.name] = &outputInfo{
|
||||||
transform: state.transform,
|
x: state.x,
|
||||||
}, true
|
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) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
|
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 {
|
if err := r.connect(); err != nil {
|
||||||
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
return nil, false, fmt.Errorf("wayland connect: %w", err)
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ func (r *RegionSelector) setupPointerHandlers() {
|
|||||||
for _, os := range r.surfaces {
|
for _, os := range r.surfaces {
|
||||||
r.redrawSurface(os)
|
r.redrawSurface(os)
|
||||||
}
|
}
|
||||||
|
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
|
||||||
|
r.finishSelection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
r.cancelled = true
|
r.cancelled = true
|
||||||
|
|||||||
@@ -138,9 +138,13 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
|
|||||||
if !r.showCapturedCursor {
|
if !r.showCapturedCursor {
|
||||||
cursorLabel = "show"
|
cursorLabel = "show"
|
||||||
}
|
}
|
||||||
|
captureKey := "Space/Enter"
|
||||||
|
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
|
||||||
|
captureKey = "Drag+Release"
|
||||||
|
}
|
||||||
|
|
||||||
items := []struct{ key, desc string }{
|
items := []struct{ key, desc string }{
|
||||||
{"Space/Enter", "capture"},
|
{captureKey, "capture"},
|
||||||
{"P", cursorLabel + " cursor"},
|
{"P", cursorLabel + " cursor"},
|
||||||
{"Esc", "cancel"},
|
{"Esc", "cancel"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -106,6 +107,12 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureRegion() (*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)
|
selector := NewRegionSelector(s)
|
||||||
result, cancelled, err := selector.Run()
|
result, cancelled, err := selector.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -298,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture all outputs first to get actual buffer sizes
|
wlrInfos := getAllOutputInfos()
|
||||||
type capturedOutput struct {
|
|
||||||
output *WaylandOutput
|
|
||||||
result *CaptureResult
|
|
||||||
physX int
|
|
||||||
physY int
|
|
||||||
}
|
|
||||||
captured := make([]capturedOutput, 0, len(outputs))
|
|
||||||
|
|
||||||
var minX, minY, maxX, maxY int
|
type pendingOutput struct {
|
||||||
first := true
|
result *CaptureResult
|
||||||
|
logX float64
|
||||||
|
logY float64
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
var pending []pendingOutput
|
||||||
|
maxScale := 1.0
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -322,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outX, outY := output.x, output.y
|
logX, logY := float64(output.x), float64(output.y)
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
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 {
|
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||||
scale = s
|
scale = hs
|
||||||
}
|
}
|
||||||
case CompositorDWL:
|
default:
|
||||||
if info, ok := getOutputInfo(output.name); ok {
|
if wlrInfos != nil {
|
||||||
outX, outY = info.x, info.y
|
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 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
physX := int(float64(outX) * scale)
|
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||||
physY := int(float64(outY) * scale)
|
if scale > maxScale {
|
||||||
|
maxScale = scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
captured = append(captured, capturedOutput{
|
if len(pending) == 0 {
|
||||||
output: output,
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
result: result,
|
}
|
||||||
physX: physX,
|
if len(pending) == 1 {
|
||||||
physY: physY,
|
return pending[0].result, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
right := physX + result.Buffer.Width
|
type layoutEntry struct {
|
||||||
bottom := physY + result.Buffer.Height
|
result *CaptureResult
|
||||||
|
canvasX int
|
||||||
|
canvasY int
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
}
|
||||||
|
entries := make([]layoutEntry, len(pending))
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
|
||||||
if first {
|
for i, p := range pending {
|
||||||
minX, minY = physX, physY
|
cx := int(math.Round(p.logX * maxScale))
|
||||||
maxX, maxY = right, bottom
|
cy := int(math.Round(p.logY * maxScale))
|
||||||
first = false
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
if cx < minX {
|
||||||
if physX < minX {
|
minX = cx
|
||||||
minX = physX
|
|
||||||
}
|
}
|
||||||
if physY < minY {
|
if cy < minY {
|
||||||
minY = physY
|
minY = cy
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
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
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||||
compositeStride := totalW * 4
|
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
c.result.Buffer.Close()
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
if format == 0 {
|
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)
|
s.blitBufferScaled(composite, e.result.Buffer,
|
||||||
c.result.Buffer.Close()
|
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||||
|
e.result.YInverted)
|
||||||
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -413,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, 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()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for srcY := 0; srcY < src.Height; srcY++ {
|
for dy := 0; dy < dstH; dy++ {
|
||||||
actualSrcY := srcY
|
canvasY := dstY + dy
|
||||||
if yInverted {
|
if canvasY < 0 || canvasY >= dst.Height {
|
||||||
actualSrcY = src.Height - 1 - srcY
|
|
||||||
}
|
|
||||||
|
|
||||||
dy := dstY + srcY
|
|
||||||
if dy < 0 || dy >= dst.Height {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := actualSrcY * src.Stride
|
srcY := dy * src.Height / dstH
|
||||||
dstRowOff := dy * dst.Stride
|
if yInverted {
|
||||||
|
srcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= src.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for srcX := 0; srcX < src.Width; srcX++ {
|
srcRowOff := srcY * src.Stride
|
||||||
dx := dstX + srcX
|
dstRowOff := canvasY * dst.Stride
|
||||||
if dx < 0 || dx >= dst.Width {
|
|
||||||
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + dx*4
|
di := dstRowOff + canvasX*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ type Config struct {
|
|||||||
Mode Mode
|
Mode Mode
|
||||||
OutputName string
|
OutputName string
|
||||||
Cursor CursorMode
|
Cursor CursorMode
|
||||||
|
NoConfirm bool
|
||||||
|
Reset bool
|
||||||
Format Format
|
Format Format
|
||||||
Quality int
|
Quality int
|
||||||
OutputDir string
|
OutputDir string
|
||||||
@@ -66,6 +68,8 @@ func DefaultConfig() Config {
|
|||||||
return Config{
|
return Config{
|
||||||
Mode: ModeRegion,
|
Mode: ModeRegion,
|
||||||
Cursor: CursorOff,
|
Cursor: CursorOff,
|
||||||
|
NoConfirm: false,
|
||||||
|
Reset: false,
|
||||||
Format: FormatPNG,
|
Format: FormatPNG,
|
||||||
Quality: 90,
|
Quality: 90,
|
||||||
OutputDir: "",
|
OutputDir: "",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func handleMatugenQueue(conn net.Conn, req models.Request) {
|
|||||||
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
SyncModeWithPortal: models.GetOr(req, "syncModeWithPortal", false),
|
||||||
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
TerminalsAlwaysDark: models.GetOr(req, "terminalsAlwaysDark", false),
|
||||||
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
SkipTemplates: models.GetOr(req, "skipTemplates", ""),
|
||||||
|
Contrast: models.GetOr(req, "contrast", 0.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
wait := models.GetOr(req, "wait", true)
|
wait := models.GetOr(req, "wait", true)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
|||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
cmdq: make(chan cmd, 128),
|
cmdq: make(chan cmd, 512),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
fatalError: make(chan error, 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
|
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
|
||||||
Package: dms
|
Package: dms
|
||||||
Architecture: amd64
|
Architecture: amd64 arm64
|
||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
dms-distropkg-amd64.gz
|
dms-distropkg-amd64.gz
|
||||||
|
dms-distropkg-arm64.gz
|
||||||
dms-source.tar.gz
|
dms-source.tar.gz
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Include files that are normally excluded by .gitignore
|
# Include files that are normally excluded by .gitignore
|
||||||
# These are needed for the build process on Launchpad
|
# These are needed for the build process on Launchpad
|
||||||
tar-ignore = !dms-distropkg-amd64.gz
|
tar-ignore = !dms-distropkg-amd64.gz
|
||||||
|
tar-ignore = !dms-distropkg-arm64.gz
|
||||||
tar-ignore = !dms-source.tar.gz
|
tar-ignore = !dms-source.tar.gz
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
%global version {{{ git_repo_version }}}
|
%global version {{{ git_repo_version }}}
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
||||||
%global go_toolchain_version 1.25.7
|
%global go_toolchain_version 1.26.1
|
||||||
|
|
||||||
Name: dms
|
Name: dms
|
||||||
Epoch: 2
|
Epoch: 2
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ let
|
|||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
compositorPackage
|
compositorPackage
|
||||||
|
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@@ -138,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 = {
|
services.greetd = {
|
||||||
enable = lib.mkDefault true;
|
enable = lib.mkDefault true;
|
||||||
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
|
||||||
@@ -195,7 +203,9 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
||||||
|
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
||||||
|
cp "$theme_file" custom-theme.json
|
||||||
mv settings.json settings.orig.json
|
mv settings.json settings.orig.json
|
||||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}: let
|
||||||
let
|
|
||||||
cfg = config.programs.dank-material-shell;
|
cfg = config.programs.dank-material-shell;
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
./dms-rename.nix
|
./dms-rename.nix
|
||||||
];
|
];
|
||||||
@@ -16,9 +14,11 @@ in
|
|||||||
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
|
||||||
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
|
||||||
includes = {
|
includes = {
|
||||||
enable = (lib.mkEnableOption "includes for niri-flake") // {
|
enable =
|
||||||
default = true;
|
(lib.mkEnableOption "includes for niri-flake")
|
||||||
};
|
// {
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
override = lib.mkOption {
|
override = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -44,8 +44,10 @@ in
|
|||||||
"alttab"
|
"alttab"
|
||||||
"binds"
|
"binds"
|
||||||
"colors"
|
"colors"
|
||||||
|
"cursor"
|
||||||
"layout"
|
"layout"
|
||||||
"outputs"
|
"outputs"
|
||||||
|
"windowrules"
|
||||||
"wpblur"
|
"wpblur"
|
||||||
];
|
];
|
||||||
example = [
|
example = [
|
||||||
@@ -70,24 +72,21 @@ in
|
|||||||
let
|
let
|
||||||
cfg' = cfg.niri.includes;
|
cfg' = cfg.niri.includes;
|
||||||
|
|
||||||
withOriginalConfig =
|
withOriginalConfig = dmsFiles:
|
||||||
dmsFiles:
|
if cfg'.override
|
||||||
if cfg'.override then
|
then [cfg'.originalFileName] ++ dmsFiles
|
||||||
[ cfg'.originalFileName ] ++ dmsFiles
|
else dmsFiles ++ [cfg'.originalFileName];
|
||||||
else
|
|
||||||
dmsFiles ++ [ cfg'.originalFileName ];
|
|
||||||
|
|
||||||
fixes = map (fix: "\n${fix}") (
|
fixes = map (fix: "\n${fix}") (
|
||||||
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
|
||||||
# kdl
|
# kdl
|
||||||
''
|
''
|
||||||
// Border fix
|
// Border fix
|
||||||
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
|
||||||
layout { border { on; }; }
|
layout { border { on; }; }
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
|
||||||
niri-config-dms = {
|
niri-config-dms = {
|
||||||
target = "niri/config.kdl";
|
target = "niri/config.kdl";
|
||||||
@@ -104,11 +103,9 @@ in
|
|||||||
|
|
||||||
programs.niri.settings = lib.mkMerge [
|
programs.niri.settings = lib.mkMerge [
|
||||||
(lib.mkIf cfg.niri.enableKeybinds {
|
(lib.mkIf cfg.niri.enableKeybinds {
|
||||||
binds =
|
binds = with config.lib.niri.actions; let
|
||||||
with config.lib.niri.actions;
|
dms-ipc = spawn "dms" "ipc";
|
||||||
let
|
in
|
||||||
dms-ipc = spawn "dms" "ipc";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
"Mod+Space" = {
|
"Mod+Space" = {
|
||||||
action = dms-ipc "spotlight" "toggle";
|
action = dms-ipc "spotlight" "toggle";
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
|
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
|
||||||
#
|
#
|
||||||
# Example:
|
# 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 questing
|
||||||
|
# ./create-source.sh ../dms-git resolute
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -25,11 +27,13 @@ if [ $# -lt 1 ]; then
|
|||||||
echo "Arguments:"
|
echo "Arguments:"
|
||||||
echo " package-dir : Path to package directory (e.g., ../dms)"
|
echo " package-dir : Path to package directory (e.g., ../dms)"
|
||||||
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
|
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
|
||||||
echo " Options: noble, jammy, oracular, mantic"
|
echo " Options: noble, jammy, oracular, mantic, questing, resolute"
|
||||||
echo
|
echo
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 ../dms questing"
|
echo " $0 ../dms questing"
|
||||||
|
echo " $0 ../dms resolute"
|
||||||
echo " $0 ../dms-git questing"
|
echo " $0 ../dms-git questing"
|
||||||
|
echo " $0 ../dms-git resolute"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -129,10 +133,14 @@ check_ppa_version_exists() {
|
|||||||
local SOURCE_NAME="$2"
|
local SOURCE_NAME="$2"
|
||||||
local VERSION="$3"
|
local VERSION="$3"
|
||||||
local CHECK_MODE="${4:-commit}"
|
local CHECK_MODE="${4:-commit}"
|
||||||
|
local DISTRO_SERIES="${5:-}"
|
||||||
|
|
||||||
# Query Launchpad API
|
# Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to questing and resolute)
|
||||||
PPA_VERSION=$(curl -s \
|
local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
|
||||||
"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 "")
|
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
|
||||||
if [[ -n "$PPA_VERSION" ]]; then
|
if [[ -n "$PPA_VERSION" ]]; then
|
||||||
@@ -259,14 +267,14 @@ if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
|
|||||||
if [[ -n "$PPA_NAME" ]]; then
|
if [[ -n "$PPA_NAME" ]]; then
|
||||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
info "Checking if version $NEW_VERSION already exists in PPA..."
|
||||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
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 "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
|
||||||
error " To rebuild with a different release number, use:"
|
error " To rebuild with a different release number, use:"
|
||||||
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
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"
|
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
|
||||||
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
NEXT_NUM=$((REBUILD_RELEASE + 1))
|
||||||
error " To rebuild with a different release number, use:"
|
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 [[ -n "$PPA_NAME" ]]; then
|
||||||
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
if [[ -z "${REBUILD_RELEASE:-}" ]]; then
|
||||||
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
|
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 "==> Error: This commit is already uploaded to PPA"
|
||||||
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
|
||||||
error " To rebuild the same commit, specify a rebuild number:"
|
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
|
PPA_NUM=$REBUILD_RELEASE
|
||||||
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
|
||||||
info "Checking if version $NEW_VERSION already exists in PPA..."
|
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 "==> Error: Version $NEW_VERSION already exists in PPA"
|
||||||
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
error " This exact version (including ppa${PPA_NUM}) is already uploaded."
|
||||||
NEXT_NUM=$((PPA_NUM + 1))
|
NEXT_NUM=$((PPA_NUM + 1))
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
PPA_OWNER="avengemedia"
|
PPA_OWNER="avengemedia"
|
||||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
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)
|
# Define packages (sync with ppa-upload.sh)
|
||||||
ALL_PACKAGES=(dms dms-git dms-greeter)
|
ALL_PACKAGES=(dms dms-git dms-greeter)
|
||||||
@@ -106,10 +107,10 @@ get_status_display() {
|
|||||||
for PPA_NAME in "${PPAS[@]}"; do
|
for PPA_NAME in "${PPAS[@]}"; do
|
||||||
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||||
|
|
||||||
|
for DISTRO_SERIES in "${DISTRO_SERIES_LIST[@]}"; do
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ==="
|
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} (Ubuntu ${DISTRO_SERIES}) ==="
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
echo "Distribution: Ubuntu $DISTRO_SERIES"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for pkg in "${PACKAGES[@]}"; do
|
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 "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
|
||||||
echo ""
|
echo ""
|
||||||
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./ppa-upload.sh dms # Single package (auto-detects PPA)
|
# ./ppa-upload.sh dms # Upload to questing + resolute (default)
|
||||||
# ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax)
|
# ./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 --rebuild=2 # Rebuild with ppa2 (flag syntax)
|
||||||
# ./ppa-upload.sh dms-git # Single package
|
# ./ppa-upload.sh dms-git # Single package (both series)
|
||||||
# ./ppa-upload.sh all # All packages
|
# ./ppa-upload.sh all # All packages (each to both series)
|
||||||
# ./ppa-upload.sh dms dms questing # Explicit PPA and series
|
# ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute")
|
||||||
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number
|
# ./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)
|
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -52,7 +54,7 @@ done
|
|||||||
|
|
||||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
||||||
|
|
||||||
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||||
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
|
||||||
@@ -64,10 +66,27 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
|||||||
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
|
||||||
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
|
||||||
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
|
||||||
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}"
|
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
|
||||||
fi
|
fi
|
||||||
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)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
|
||||||
@@ -119,7 +138,12 @@ elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
|
|||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
info "Processing $pkg..."
|
info "Processing $pkg..."
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
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")
|
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||||
if ! "$0" "${BUILD_ARGS[@]}"; then
|
if ! "$0" "${BUILD_ARGS[@]}"; then
|
||||||
FAILED_PACKAGES+=("$pkg")
|
FAILED_PACKAGES+=("$pkg")
|
||||||
@@ -165,7 +189,9 @@ else
|
|||||||
|
|
||||||
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
|
||||||
PACKAGE_INPUT="all"
|
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")
|
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
|
||||||
exec "$0" "${BUILD_ARGS[@]}"
|
exec "$0" "${BUILD_ARGS[@]}"
|
||||||
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
|
||||||
@@ -191,6 +217,48 @@ fi
|
|||||||
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||||
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
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 "Building and uploading: $PACKAGE_NAME"
|
||||||
info "Package directory: $PACKAGE_DIR"
|
info "Package directory: $PACKAGE_DIR"
|
||||||
info "PPA: ppa:avengemedia/$PPA_NAME"
|
info "PPA: ppa:avengemedia/$PPA_NAME"
|
||||||
|
|||||||
@@ -538,6 +538,8 @@ Color picker modal control.
|
|||||||
|
|
||||||
**Functions:**
|
**Functions:**
|
||||||
- `open` - Show color picker modal
|
- `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
|
- `close` - Hide color picker modal
|
||||||
- `closeInstant` - Hide color picker modal without animation
|
- `closeInstant` - Hide color picker modal without animation
|
||||||
- `toggle` - Toggle color picker modal visibility
|
- `toggle` - Toggle color picker modal visibility
|
||||||
|
|||||||
@@ -150,6 +150,9 @@
|
|||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--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 \
|
installShellCompletion --cmd dms \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--bash <($out/bin/dms completion bash) \
|
||||||
--fish <($out/bin/dms completion fish) \
|
--fish <($out/bin/dms completion fish) \
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Singleton {
|
|||||||
|
|
||||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[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 data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||||
@@ -72,7 +73,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveIconPath(iconName: string): string {
|
function resolveIconPath(iconName: string): string {
|
||||||
if (!iconName) return "";
|
if (!iconName)
|
||||||
|
return "";
|
||||||
const moddedId = moddedAppId(iconName);
|
const moddedId = moddedAppId(iconName);
|
||||||
if (moddedId !== iconName) {
|
if (moddedId !== iconName) {
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||||
@@ -85,7 +87,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveIconUrl(iconName: string): string {
|
function resolveIconUrl(iconName: string): string {
|
||||||
if (!iconName) return "";
|
if (!iconName)
|
||||||
|
return "";
|
||||||
const moddedId = moddedAppId(iconName);
|
const moddedId = moddedAppId(iconName);
|
||||||
if (moddedId !== iconName) {
|
if (moddedId !== iconName) {
|
||||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||||
@@ -98,7 +101,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
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");
|
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +122,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAppName(appId: string, desktopEntry: var): string {
|
function getAppName(appId: string, desktopEntry: var): string {
|
||||||
if (appId === "org.quickshell") {
|
if (appId === "org.quickshell" || appId === "com.danklinux.dms") {
|
||||||
return "dms";
|
return "dms";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -12,6 +13,37 @@ Singleton {
|
|||||||
signal popoutOpening
|
signal popoutOpening
|
||||||
signal popoutChanged
|
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) {
|
function showPopout(popout) {
|
||||||
if (!popout || !popout.screen)
|
if (!popout || !popout.screen)
|
||||||
return;
|
return;
|
||||||
@@ -23,13 +55,11 @@ Singleton {
|
|||||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||||
if (!otherPopout || otherPopout === popout)
|
if (!otherPopout || otherPopout === popout)
|
||||||
continue;
|
continue;
|
||||||
if (otherPopout.dashVisible !== undefined) {
|
if (_isStale(otherPopout)) {
|
||||||
otherPopout.dashVisible = false;
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
continue;
|
||||||
otherPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
otherPopout.close();
|
|
||||||
}
|
}
|
||||||
|
_closePopout(otherPopout);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPopoutsByScreen[screenName] = popout;
|
currentPopoutsByScreen[screenName] = popout;
|
||||||
@@ -51,15 +81,9 @@ Singleton {
|
|||||||
function closeAllPopouts() {
|
function closeAllPopouts() {
|
||||||
for (const screenName in currentPopoutsByScreen) {
|
for (const screenName in currentPopoutsByScreen) {
|
||||||
const popout = currentPopoutsByScreen[screenName];
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
if (!popout)
|
if (!popout || _isStale(popout))
|
||||||
continue;
|
continue;
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentPopoutsByScreen = {};
|
currentPopoutsByScreen = {};
|
||||||
}
|
}
|
||||||
@@ -90,6 +114,12 @@ Singleton {
|
|||||||
if (!otherPopout)
|
if (!otherPopout)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (_isStale(otherPopout)) {
|
||||||
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
|
currentPopoutTriggers[otherScreenName] = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (otherPopout === popout) {
|
if (otherPopout === popout) {
|
||||||
movedFromOtherScreen = true;
|
movedFromOtherScreen = true;
|
||||||
currentPopoutsByScreen[otherScreenName] = null;
|
currentPopoutsByScreen[otherScreenName] = null;
|
||||||
@@ -97,45 +127,26 @@ Singleton {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherPopout.dashVisible !== undefined) {
|
_closePopout(otherPopout);
|
||||||
otherPopout.dashVisible = false;
|
|
||||||
} else if (otherPopout.notificationHistoryVisible !== undefined) {
|
|
||||||
otherPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
otherPopout.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout && currentPopout !== popout) {
|
if (currentPopout && currentPopout !== popout) {
|
||||||
if (currentPopout.dashVisible !== undefined) {
|
if (_isStale(currentPopout)) {
|
||||||
currentPopout.dashVisible = false;
|
currentPopoutsByScreen[screenName] = null;
|
||||||
} else if (currentPopout.notificationHistoryVisible !== undefined) {
|
currentPopoutTriggers[screenName] = null;
|
||||||
currentPopout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
} else {
|
||||||
currentPopout.close();
|
_closePopout(currentPopout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerId === undefined) {
|
if (triggerId === undefined) {
|
||||||
if (popout.dashVisible !== undefined) {
|
_closePopout(popout);
|
||||||
popout.dashVisible = false;
|
|
||||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
|
||||||
popout.notificationHistoryVisible = false;
|
|
||||||
} else {
|
|
||||||
popout.close();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,8 +132,12 @@ Singleton {
|
|||||||
property string timeLocale: ""
|
property string timeLocale: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
|
property string launcherLastQuery: ""
|
||||||
|
property var launcherQueryHistory: []
|
||||||
property string appDrawerLastMode: "apps"
|
property string appDrawerLastMode: "apps"
|
||||||
property string niriOverviewLastMode: "apps"
|
property string niriOverviewLastMode: "apps"
|
||||||
|
property string settingsSidebarExpandedIds: ","
|
||||||
|
property string settingsSidebarCollapsedIds: ","
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -343,8 +347,8 @@ Singleton {
|
|||||||
|
|
||||||
function setLightMode(lightMode) {
|
function setLightMode(lightMode) {
|
||||||
isSwitchingMode = true;
|
isSwitchingMode = true;
|
||||||
|
syncWallpaperForCurrentMode(lightMode);
|
||||||
isLightMode = lightMode;
|
isLightMode = lightMode;
|
||||||
syncWallpaperForCurrentMode();
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
isSwitchingMode = false;
|
isSwitchingMode = false;
|
||||||
@@ -1094,6 +1098,43 @@ Singleton {
|
|||||||
saveSettings();
|
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) {
|
function setAppDrawerLastMode(mode) {
|
||||||
appDrawerLastMode = mode;
|
appDrawerLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -1104,15 +1145,22 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncWallpaperForCurrentMode() {
|
function setSettingsSidebarState(expandedIds, collapsedIds) {
|
||||||
|
settingsSidebarExpandedIds = expandedIds;
|
||||||
|
settingsSidebarCollapsedIds = collapsedIds;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncWallpaperForCurrentMode(mode) {
|
||||||
if (!perModeWallpaper)
|
if (!perModeWallpaper)
|
||||||
return;
|
return;
|
||||||
|
var light = (mode !== undefined) ? mode : isLightMode;
|
||||||
if (perMonitorWallpaper) {
|
if (perMonitorWallpaper) {
|
||||||
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
monitorWallpapers = light ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark;
|
wallpaperPath = light ? wallpaperPathLight : wallpaperPathDark;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _findMonitorValue(map, screenName) {
|
function _findMonitorValue(map, screenName) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
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"
|
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 string customThemeFile: ""
|
||||||
property var registryThemeVariants: ({})
|
property var registryThemeVariants: ({})
|
||||||
property string matugenScheme: "scheme-tonal-spot"
|
property string matugenScheme: "scheme-tonal-spot"
|
||||||
|
property real matugenContrast: 0
|
||||||
property bool runUserMatugenTemplates: true
|
property bool runUserMatugenTemplates: true
|
||||||
property string matugenTargetMonitor: ""
|
property string matugenTargetMonitor: ""
|
||||||
property real popupTransparency: 1.0
|
property real popupTransparency: 1.0
|
||||||
@@ -150,6 +151,7 @@ Singleton {
|
|||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
|
|
||||||
property int firstDayOfWeek: -1
|
property int firstDayOfWeek: -1
|
||||||
|
property bool showWeekNumber: false
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
@@ -184,10 +186,46 @@ Singleton {
|
|||||||
onPopoutElevationEnabledChanged: saveSettings()
|
onPopoutElevationEnabledChanged: saveSettings()
|
||||||
property bool barElevationEnabled: true
|
property bool barElevationEnabled: true
|
||||||
onBarElevationEnabledChanged: saveSettings()
|
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 string wallpaperFillMode: "Fill"
|
||||||
property bool blurredWallpaperLayer: false
|
property bool blurredWallpaperLayer: false
|
||||||
property bool blurWallpaperOnOverview: 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 showLauncherButton: true
|
||||||
property bool showWorkspaceSwitcher: true
|
property bool showWorkspaceSwitcher: true
|
||||||
property bool showFocusedWindow: true
|
property bool showFocusedWindow: true
|
||||||
@@ -336,6 +374,7 @@ Singleton {
|
|||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
|
property bool rememberLastQuery: false
|
||||||
property var spotlightSectionViewModes: ({})
|
property var spotlightSectionViewModes: ({})
|
||||||
onSpotlightSectionViewModesChanged: saveSettings()
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
property var appDrawerSectionViewModes: ({})
|
property var appDrawerSectionViewModes: ({})
|
||||||
@@ -453,6 +492,11 @@ Singleton {
|
|||||||
property bool syncModeWithPortal: true
|
property bool syncModeWithPortal: true
|
||||||
property bool terminalsAlwaysDark: false
|
property bool terminalsAlwaysDark: false
|
||||||
|
|
||||||
|
property string muxType: "tmux"
|
||||||
|
property bool muxUseCustomCommand: false
|
||||||
|
property string muxCustomCommand: ""
|
||||||
|
property string muxSessionFilter: ""
|
||||||
|
|
||||||
property bool runDmsMatugenTemplates: true
|
property bool runDmsMatugenTemplates: true
|
||||||
property bool matugenTemplateGtk: true
|
property bool matugenTemplateGtk: true
|
||||||
property bool matugenTemplateNiri: true
|
property bool matugenTemplateNiri: true
|
||||||
@@ -478,9 +522,16 @@ Singleton {
|
|||||||
property bool matugenTemplateZed: true
|
property bool matugenTemplateZed: true
|
||||||
|
|
||||||
property var matugenTemplateNeovimSettings: ({
|
property var matugenTemplateNeovimSettings: ({
|
||||||
"dark": { "baseTheme": "github_dark", "harmony": 0.5 },
|
"dark": {
|
||||||
"light": { "baseTheme": "github_light", "harmony": 0.5 }
|
"baseTheme": "github_dark",
|
||||||
})
|
"harmony": 0.5
|
||||||
|
},
|
||||||
|
"light": {
|
||||||
|
"baseTheme": "github_light",
|
||||||
|
"harmony": 0.5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
property bool matugenTemplateNeovimSetBackground: true
|
||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
@@ -1189,13 +1240,23 @@ Singleton {
|
|||||||
Quickshell.execDetached(["sh", "-lc", script]);
|
Quickshell.execDetached(["sh", "-lc", script]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleAuthApply() {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
Processes.settingsRoot = root;
|
||||||
|
Processes.scheduleAuthApply();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"applyStoredTheme": applyStoredTheme,
|
"applyStoredTheme": applyStoredTheme,
|
||||||
"regenSystemThemes": regenSystemThemes,
|
"regenSystemThemes": regenSystemThemes,
|
||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
|
"scheduleAuthApply": scheduleAuthApply
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
@@ -1314,9 +1375,7 @@ Singleton {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
const msg = String(error || "").toLowerCase();
|
const msg = String(error || "").toLowerCase();
|
||||||
return msg.indexOf("file does not exist") !== -1
|
return msg.indexOf("file does not exist") !== -1 || msg.indexOf("no such file") !== -1 || msg.indexOf("enoent") !== -1;
|
||||||
|| msg.indexOf("no such file") !== -1
|
|
||||||
|| msg.indexOf("enoent") !== -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPluginSettings() {
|
function loadPluginSettings() {
|
||||||
@@ -1907,6 +1966,66 @@ Singleton {
|
|||||||
return filtered;
|
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() {
|
function sendTestNotifications() {
|
||||||
NotificationService.dismissAllPopups();
|
NotificationService.dismissAllPopups();
|
||||||
sendTestNotification(0);
|
sendTestNotification(0);
|
||||||
@@ -1936,6 +2055,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMatugenContrast(value) {
|
||||||
|
if (matugenContrast === value)
|
||||||
|
return;
|
||||||
|
set("matugenContrast", value);
|
||||||
|
}
|
||||||
|
|
||||||
function setRunUserMatugenTemplates(enabled) {
|
function setRunUserMatugenTemplates(enabled) {
|
||||||
if (runUserMatugenTemplates === enabled)
|
if (runUserMatugenTemplates === enabled)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1248,7 +1248,8 @@ Singleton {
|
|||||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||||
const defaults = themeData.variants.defaults || {};
|
const defaults = themeData.variants.defaults || {};
|
||||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||||
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const stored = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
||||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||||
const accentId = stored.accent || modeDefaults.accent || "";
|
const accentId = stored.accent || modeDefaults.accent || "";
|
||||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||||
@@ -1274,7 +1275,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
||||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||||
@@ -1547,6 +1549,9 @@ Singleton {
|
|||||||
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
if (typeof SettingsData !== "undefined" && SettingsData.terminalsAlwaysDark) {
|
||||||
args.push("--terminals-always-dark");
|
args.push("--terminals-always-dark");
|
||||||
}
|
}
|
||||||
|
if (typeof SettingsData !== "undefined" && SettingsData.matugenContrast !== 0) {
|
||||||
|
args.push("--contrast", SettingsData.matugenContrast.toString());
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof SettingsData !== "undefined") {
|
if (typeof SettingsData !== "undefined") {
|
||||||
const skipTemplates = [];
|
const skipTemplates = [];
|
||||||
@@ -1646,8 +1651,9 @@ Singleton {
|
|||||||
const defaults = customThemeRawData.variants.defaults || {};
|
const defaults = customThemeRawData.variants.defaults || {};
|
||||||
const darkDefaults = defaults.dark || {};
|
const darkDefaults = defaults.dark || {};
|
||||||
const lightDefaults = defaults.light || defaults.dark || {};
|
const lightDefaults = defaults.light || defaults.dark || {};
|
||||||
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
const storedDark = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
||||||
|
const storedLight = isGreeterMode ? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
@@ -1665,7 +1671,8 @@ Singleton {
|
|||||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode ? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default) : (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
||||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||||
@@ -1993,6 +2000,7 @@ Singleton {
|
|||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
blockLoading: false
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: !SessionData.isGreeterMode
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
|
|||||||
@@ -1249,7 +1249,7 @@ const defaultOpts = {
|
|||||||
};
|
};
|
||||||
class Finder {
|
class Finder {
|
||||||
constructor(list, ...optionsTuple) {
|
constructor(list, ...optionsTuple) {
|
||||||
this.opts = Object.assign(defaultOpts, optionsTuple[0]);
|
this.opts = Object.assign({}, defaultOpts, optionsTuple[0]);
|
||||||
this.items = list;
|
this.items = list;
|
||||||
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
|
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
|
||||||
this.algoFn = exactMatchNaive;
|
this.algoFn = exactMatchNaive;
|
||||||
@@ -1283,12 +1283,13 @@ function postProcessResultItems(result, opts) {
|
|||||||
if (opts.sort) {
|
if (opts.sort) {
|
||||||
const { selector } = opts;
|
const { selector } = opts;
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
if (a.score === b.score) {
|
if (a.score !== b.score) {
|
||||||
for (const tiebreaker of opts.tiebreakers) {
|
return b.score - a.score;
|
||||||
const diff = tiebreaker(a, b, selector);
|
}
|
||||||
if (diff !== 0) {
|
for (const tiebreaker of opts.tiebreakers) {
|
||||||
return diff;
|
const diff = tiebreaker(a, b, selector);
|
||||||
}
|
if (diff !== 0) {
|
||||||
|
return diff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -52,6 +54,14 @@ Singleton {
|
|||||||
|
|
||||||
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||||
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_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() {
|
function detectQtTools() {
|
||||||
qtToolsDetectionProcess.running = true;
|
qtToolsDetectionProcess.running = true;
|
||||||
@@ -70,14 +80,12 @@ Singleton {
|
|||||||
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
|
pamFprintSupportDetected = false;
|
||||||
pamFprintSupportDetected = false;
|
pamU2fSupportDetected = false;
|
||||||
pamU2fSupportDetected = false;
|
pamSupportProbeOutput = "";
|
||||||
pamSupportProbeOutput = "";
|
pamSupportProbeStreamFinished = false;
|
||||||
pamSupportProbeStreamFinished = false;
|
pamSupportProbeExited = false;
|
||||||
pamSupportProbeExited = false;
|
pamSupportDetectionProcess.running = true;
|
||||||
pamSupportDetectionProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
recomputeAuthCapabilities();
|
recomputeAuthCapabilities();
|
||||||
}
|
}
|
||||||
@@ -94,6 +102,50 @@ Singleton {
|
|||||||
pluginSettingsCheckProcess.running = true;
|
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) {
|
function stripPamComment(line) {
|
||||||
if (!line)
|
if (!line)
|
||||||
return "";
|
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 {
|
FileView {
|
||||||
id: greetdPamWatcher
|
id: greetdPamWatcher
|
||||||
path: "/etc/pam.d/greetd"
|
path: "/etc/pam.d/greetd"
|
||||||
|
|||||||
@@ -83,8 +83,13 @@ var SPEC = {
|
|||||||
timeLocale: { def: "" },
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
|
launcherLastQuery: { def: "" },
|
||||||
|
launcherQueryHistory: { def: [] },
|
||||||
appDrawerLastMode: { def: "apps" },
|
appDrawerLastMode: { def: "apps" },
|
||||||
niriOverviewLastMode: { def: "apps" }
|
niriOverviewLastMode: { def: "apps" },
|
||||||
|
|
||||||
|
settingsSidebarExpandedIds: { def: "," },
|
||||||
|
settingsSidebarCollapsedIds: { def: "," }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var SPEC = {
|
|||||||
customThemeFile: { def: "" },
|
customThemeFile: { def: "" },
|
||||||
registryThemeVariants: { def: {} },
|
registryThemeVariants: { def: {} },
|
||||||
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
matugenScheme: { def: "scheme-tonal-spot", onChange: "regenSystemThemes" },
|
||||||
|
matugenContrast: { def: 0, onChange: "regenSystemThemes" },
|
||||||
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
|
||||||
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ var SPEC = {
|
|||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
|
||||||
firstDayOfWeek: { def: -1 },
|
firstDayOfWeek: { def: -1 },
|
||||||
|
showWeekNumber: { def: false },
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
padHours12Hour: { def: false },
|
padHours12Hour: { def: false },
|
||||||
@@ -56,6 +58,10 @@ var SPEC = {
|
|||||||
modalElevationEnabled: { def: true },
|
modalElevationEnabled: { def: true },
|
||||||
popoutElevationEnabled: { def: true },
|
popoutElevationEnabled: { def: true },
|
||||||
barElevationEnabled: { def: true },
|
barElevationEnabled: { def: true },
|
||||||
|
blurEnabled: { def: false },
|
||||||
|
blurBorderColor: { def: "outline" },
|
||||||
|
blurBorderCustomColor: { def: "#ffffff" },
|
||||||
|
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
|
||||||
wallpaperFillMode: { def: "Fill" },
|
wallpaperFillMode: { def: "Fill" },
|
||||||
blurredWallpaperLayer: { def: false },
|
blurredWallpaperLayer: { def: false },
|
||||||
blurWallpaperOnOverview: { def: false },
|
blurWallpaperOnOverview: { def: false },
|
||||||
@@ -167,8 +173,8 @@ var SPEC = {
|
|||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
greeterRememberLastSession: { def: true },
|
||||||
greeterRememberLastUser: { def: true },
|
greeterRememberLastUser: { def: true },
|
||||||
greeterEnableFprint: { def: false },
|
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterEnableU2f: { def: false },
|
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
greeterWallpaperPath: { def: "" },
|
greeterWallpaperPath: { def: "" },
|
||||||
greeterUse24HourClock: { def: true },
|
greeterUse24HourClock: { def: true },
|
||||||
greeterShowSeconds: { def: false },
|
greeterShowSeconds: { def: false },
|
||||||
@@ -187,6 +193,7 @@ var SPEC = {
|
|||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
|
rememberLastQuery: { def: false },
|
||||||
spotlightSectionViewModes: { def: {} },
|
spotlightSectionViewModes: { def: {} },
|
||||||
appDrawerSectionViewModes: { def: {} },
|
appDrawerSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
@@ -268,6 +275,11 @@ var SPEC = {
|
|||||||
syncModeWithPortal: { def: true },
|
syncModeWithPortal: { def: true },
|
||||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||||
|
|
||||||
|
muxType: { def: "tmux" },
|
||||||
|
muxUseCustomCommand: { def: false },
|
||||||
|
muxCustomCommand: { def: "" },
|
||||||
|
muxSessionFilter: { def: "" },
|
||||||
|
|
||||||
runDmsMatugenTemplates: { def: true },
|
runDmsMatugenTemplates: { def: true },
|
||||||
matugenTemplateGtk: { def: true },
|
matugenTemplateGtk: { def: true },
|
||||||
matugenTemplateNiri: { def: true },
|
matugenTemplateNiri: { def: true },
|
||||||
@@ -293,11 +305,12 @@ var SPEC = {
|
|||||||
matugenTemplateZed: { def: true },
|
matugenTemplateZed: { def: true },
|
||||||
|
|
||||||
matugenTemplateNeovimSettings: {
|
matugenTemplateNeovimSettings: {
|
||||||
def: {
|
def: {
|
||||||
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
dark: { baseTheme: "github_dark", harmony: 0.5 },
|
||||||
light: { baseTheme: "github_light", harmony: 0.5 }
|
light: { baseTheme: "github_light", harmony: 0.5 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
matugenTemplateNeovimSetBackground: { def: true },
|
||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
@@ -345,7 +358,7 @@ var SPEC = {
|
|||||||
lockScreenShowMediaPlayer: { def: true },
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
lockAtStartup: { def: false },
|
lockAtStartup: { def: false },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
lockFingerprintCanEnable: { def: false, persist: false },
|
lockFingerprintCanEnable: { def: false, persist: false },
|
||||||
@@ -355,7 +368,7 @@ var SPEC = {
|
|||||||
greeterFingerprintReady: { def: false, persist: false },
|
greeterFingerprintReady: { def: false, persist: false },
|
||||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
greeterFingerprintSource: { def: "none", persist: false },
|
greeterFingerprintSource: { def: "none", persist: false },
|
||||||
enableU2f: { def: false },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
u2fAvailable: { def: false, persist: false },
|
u2fAvailable: { def: false, persist: false },
|
||||||
lockU2fCanEnable: { def: false, persist: false },
|
lockU2fCanEnable: { def: false, persist: false },
|
||||||
@@ -534,7 +547,17 @@ var SPEC = {
|
|||||||
clipboardEnterToPaste: { def: false },
|
clipboardEnterToPaste: { def: false },
|
||||||
|
|
||||||
launcherPluginVisibility: { def: {} },
|
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() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
|
|||||||
settings.configVersion = 6;
|
settings.configVersion = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 11) {
|
||||||
|
settings.configVersion = 11;
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import qs.Modules.OSD
|
|||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
import qs.Modules.DankBar
|
import qs.Modules.DankBar
|
||||||
import qs.Modules.DankBar.Popouts
|
import qs.Modules.DankBar.Popouts
|
||||||
|
import qs.Modules.Frame
|
||||||
import qs.Modules.WorkspaceOverlays
|
import qs.Modules.WorkspaceOverlays
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -176,6 +177,8 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Frame {}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dankBarRepeater
|
id: dankBarRepeater
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
@@ -619,6 +622,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MuxModal {
|
||||||
|
id: muxModal
|
||||||
|
}
|
||||||
|
|
||||||
ClipboardHistoryModal {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryModalPopup
|
id: clipboardHistoryModalPopup
|
||||||
|
|
||||||
@@ -815,9 +822,8 @@ Item {
|
|||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Notepad {
|
Notepad {
|
||||||
onHideRequested: {
|
slideout: notepadSlideout
|
||||||
notepadSlideout.hide();
|
onHideRequested: notepadSlideout.hide()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ Rectangle {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
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
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Quickshell
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -30,7 +31,7 @@ Item {
|
|||||||
property real animationOffset: Theme.spacingL
|
property real animationOffset: Theme.spacingL
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
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 color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
@@ -59,11 +60,25 @@ Item {
|
|||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
const focusedScreen = CompositorService.getFocusedScreen();
|
const focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||||
if (focusedScreen) {
|
if (focusedScreen) {
|
||||||
|
if (screenChanged)
|
||||||
|
contentWindow.visible = false;
|
||||||
contentWindow.screen = focusedScreen;
|
contentWindow.screen = focusedScreen;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow) {
|
||||||
|
if (screenChanged)
|
||||||
|
clickCatcher.visible = false;
|
||||||
clickCatcher.screen = focusedScreen;
|
clickCatcher.screen = focusedScreen;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (screenChanged) {
|
||||||
|
Qt.callLater(() => root._finishOpen());
|
||||||
|
} else {
|
||||||
|
_finishOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _finishOpen() {
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
shouldBeVisible = true;
|
shouldBeVisible = true;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow)
|
||||||
@@ -215,6 +230,16 @@ Item {
|
|||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
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.namespace: root.layerNamespace
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: {
|
||||||
if (root.useOverlayLayer)
|
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"
|
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 {
|
FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: root.shouldBeVisible
|
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
|
modalWidth: 680
|
||||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
||||||
backgroundColor: Theme.surfaceContainer
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
@@ -147,6 +147,13 @@ DankModal {
|
|||||||
return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
|
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 {
|
function close(): string {
|
||||||
root.hide();
|
root.hide();
|
||||||
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
|
||||||
|
|||||||
@@ -207,9 +207,12 @@ Rectangle {
|
|||||||
selectedActionIndex = 0;
|
selectedActionIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleAction() {
|
function cycleAction(reverse = false) {
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
if (! reverse)
|
||||||
|
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
|
||||||
|
else
|
||||||
|
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
|
||||||
ensureSelectedVisible();
|
ensureSelectedVisible();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,14 @@ Item {
|
|||||||
signal itemExecuted
|
signal itemExecuted
|
||||||
signal searchCompleted
|
signal searchCompleted
|
||||||
signal modeChanged(string mode)
|
signal modeChanged(string mode)
|
||||||
|
signal queryChanged(string query)
|
||||||
signal viewModeChanged(string sectionId, string mode)
|
signal viewModeChanged(string sectionId, string mode)
|
||||||
signal searchQueryRequested(string query)
|
signal searchQueryRequested(string query)
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
|
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
selectedItem = null;
|
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 fileSearchType: "all"
|
||||||
property string fileSearchExt: ""
|
property string fileSearchExt: ""
|
||||||
property string fileSearchFolder: ""
|
property string fileSearchFolder: ""
|
||||||
@@ -353,10 +383,13 @@ Item {
|
|||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleMode() {
|
function cycleMode(reverse = false) {
|
||||||
var modes = ["all", "apps", "files", "plugins"];
|
var modes = ["all", "apps", "files", "plugins"];
|
||||||
var currentIndex = modes.indexOf(searchMode);
|
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]);
|
setMode(modes[nextIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +526,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performSearch() {
|
function performSearch() {
|
||||||
|
queryChanged(searchQuery);
|
||||||
|
|
||||||
var currentVersion = _searchVersion;
|
var currentVersion = _searchVersion;
|
||||||
isSearching = true;
|
isSearching = true;
|
||||||
var shouldResetSelection = _queryDrivenSearch;
|
var shouldResetSelection = _queryDrivenSearch;
|
||||||
@@ -1651,6 +1686,9 @@ Item {
|
|||||||
function executeItem(item) {
|
function executeItem(item) {
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
|
|
||||||
if (item.type === "plugin_browse") {
|
if (item.type === "plugin_browse") {
|
||||||
var browsePluginId = item.data?.pluginId;
|
var browsePluginId = item.data?.pluginId;
|
||||||
if (!browsePluginId)
|
if (!browsePluginId)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,7 +17,6 @@ Item {
|
|||||||
property var spotlightContent: launcherContentLoader.item
|
property var spotlightContent: launcherContentLoader.item
|
||||||
property bool openedFromOverview: false
|
property bool openedFromOverview: false
|
||||||
property bool isClosing: false
|
property bool isClosing: false
|
||||||
property bool _windowEnabled: true
|
|
||||||
property bool _pendingInitialize: false
|
property bool _pendingInitialize: false
|
||||||
property string _pendingQuery: ""
|
property string _pendingQuery: ""
|
||||||
property string _pendingMode: ""
|
property string _pendingMode: ""
|
||||||
@@ -99,8 +98,16 @@ Item {
|
|||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
|
|
||||||
|
var targetQuery = "";
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
targetQuery = query;
|
||||||
|
} else if (SettingsData.rememberLastQuery) {
|
||||||
|
targetQuery = SessionData.launcherLastQuery || "";
|
||||||
|
}
|
||||||
|
|
||||||
if (spotlightContent.searchField) {
|
if (spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.text = query;
|
spotlightContent.searchField.text = targetQuery;
|
||||||
}
|
}
|
||||||
if (spotlightContent.controller) {
|
if (spotlightContent.controller) {
|
||||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||||
@@ -115,12 +122,10 @@ Item {
|
|||||||
spotlightContent.controller.collapsedSections = {};
|
spotlightContent.controller.collapsedSections = {};
|
||||||
spotlightContent.controller.selectedFlatIndex = 0;
|
spotlightContent.controller.selectedFlatIndex = 0;
|
||||||
spotlightContent.controller.selectedItem = null;
|
spotlightContent.controller.selectedItem = null;
|
||||||
if (query) {
|
spotlightContent.controller.historyIndex = -1;
|
||||||
spotlightContent.controller.setSearchQuery(query);
|
spotlightContent.controller.searchQuery = targetQuery;
|
||||||
} else {
|
|
||||||
spotlightContent.controller.searchQuery = "";
|
spotlightContent.controller.performSearch();
|
||||||
spotlightContent.controller.performSearch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (spotlightContent.resetScroll) {
|
if (spotlightContent.resetScroll) {
|
||||||
spotlightContent.resetScroll();
|
spotlightContent.resetScroll();
|
||||||
@@ -130,40 +135,47 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function _finishShow(query, mode) {
|
||||||
closeCleanupTimer.stop();
|
spotlightOpen = true;
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
|
||||||
if (focusedScreen)
|
|
||||||
launcherWindow.screen = focusedScreen;
|
|
||||||
|
|
||||||
spotlightOpen = true;
|
|
||||||
keyboardActive = true;
|
keyboardActive = true;
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
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) {
|
function showWithQuery(query) {
|
||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
isClosing = false;
|
|
||||||
openedFromOverview = false;
|
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
if (focusedScreen)
|
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||||
|
spotlightOpen = false;
|
||||||
|
isClosing = false;
|
||||||
launcherWindow.screen = focusedScreen;
|
launcherWindow.screen = focusedScreen;
|
||||||
|
Qt.callLater(() => root._finishShow(query, ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spotlightOpen = true;
|
_finishShow(query, "");
|
||||||
keyboardActive = true;
|
|
||||||
ModalManager.openModal(root);
|
|
||||||
if (useHyprlandFocusGrab)
|
|
||||||
focusGrab.active = true;
|
|
||||||
|
|
||||||
_ensureContentLoadedAndInitialize(query, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -187,14 +199,20 @@ Item {
|
|||||||
|
|
||||||
function showWithMode(mode) {
|
function showWithMode(mode) {
|
||||||
closeCleanupTimer.stop();
|
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;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
|
||||||
if (focusedScreen)
|
|
||||||
launcherWindow.screen = focusedScreen;
|
|
||||||
|
|
||||||
spotlightOpen = true;
|
|
||||||
keyboardActive = true;
|
keyboardActive = true;
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
@@ -233,6 +251,7 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
|
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode) {
|
||||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||||
return;
|
return;
|
||||||
@@ -267,41 +286,39 @@ Item {
|
|||||||
if (Quickshell.screens.length === 0)
|
if (Quickshell.screens.length === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const screen = launcherWindow.screen;
|
const screenName = launcherWindow.screen?.name;
|
||||||
const screenName = screen?.name;
|
if (screenName) {
|
||||||
|
|
||||||
let needsReset = !screen || !screenName;
|
|
||||||
if (!needsReset) {
|
|
||||||
needsReset = true;
|
|
||||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||||
if (Quickshell.screens[i].name === screenName) {
|
if (Quickshell.screens[i].name === screenName)
|
||||||
needsReset = false;
|
return;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!needsReset)
|
if (spotlightOpen)
|
||||||
return;
|
hide();
|
||||||
|
|
||||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||||
if (!newScreen)
|
if (newScreen)
|
||||||
return;
|
launcherWindow.screen = newScreen;
|
||||||
|
|
||||||
root._windowEnabled = false;
|
|
||||||
launcherWindow.screen = newScreen;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root._windowEnabled = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: launcherWindow
|
id: launcherWindow
|
||||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
visible: spotlightOpen || isClosing
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusionMode: ExclusionMode.Ignore
|
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.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: {
|
||||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||||
@@ -435,6 +452,14 @@ Item {
|
|||||||
event.accepted = true;
|
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 || "";
|
editCommentField.text = existing?.comment || "";
|
||||||
editEnvVarsField.text = existing?.envVars || "";
|
editEnvVarsField.text = existing?.envVars || "";
|
||||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||||
|
editDgpuToggle.checked = existing?.launchOnDgpu || false;
|
||||||
editMode = true;
|
editMode = true;
|
||||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||||
}
|
}
|
||||||
@@ -64,6 +65,8 @@ FocusScope {
|
|||||||
override.envVars = editEnvVarsField.text.trim();
|
override.envVars = editEnvVarsField.text.trim();
|
||||||
if (editExtraFlagsField.text.trim())
|
if (editExtraFlagsField.text.trim())
|
||||||
override.extraFlags = editExtraFlagsField.text.trim();
|
override.extraFlags = editExtraFlagsField.text.trim();
|
||||||
|
if (editDgpuToggle.checked)
|
||||||
|
override.launchOnDgpu = true;
|
||||||
SessionData.setAppOverride(editAppId, override);
|
SessionData.setAppOverride(editAppId, override);
|
||||||
closeEditMode();
|
closeEditMode();
|
||||||
}
|
}
|
||||||
@@ -146,10 +149,18 @@ FocusScope {
|
|||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Down:
|
case Qt.Key_Down:
|
||||||
controller.selectNext();
|
if (hasCtrl) {
|
||||||
|
controller.navigateHistory("down");
|
||||||
|
} else {
|
||||||
|
controller.selectNext();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Up:
|
case Qt.Key_Up:
|
||||||
controller.selectPrevious();
|
if (hasCtrl) {
|
||||||
|
controller.navigateHistory("up");
|
||||||
|
} else {
|
||||||
|
controller.selectPrevious();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case Qt.Key_PageDown:
|
case Qt.Key_PageDown:
|
||||||
controller.selectPageDown(8);
|
controller.selectPageDown(8);
|
||||||
@@ -158,6 +169,10 @@ FocusScope {
|
|||||||
controller.selectPageUp(8);
|
controller.selectPageUp(8);
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Right:
|
case Qt.Key_Right:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.cycleMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
controller.selectRight();
|
controller.selectRight();
|
||||||
return;
|
return;
|
||||||
@@ -165,12 +180,25 @@ FocusScope {
|
|||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Left:
|
case Qt.Key_Left:
|
||||||
|
if (hasCtrl) {
|
||||||
|
const reverse = true;
|
||||||
|
controller.cycleMode(reverse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||||
controller.selectLeft();
|
controller.selectLeft();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_H:
|
||||||
|
if (hasCtrl) {
|
||||||
|
const reverse = true;
|
||||||
|
controller.cycleMode(reverse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
case Qt.Key_J:
|
case Qt.Key_J:
|
||||||
if (hasCtrl) {
|
if (hasCtrl) {
|
||||||
controller.selectNext();
|
controller.selectNext();
|
||||||
@@ -185,6 +213,13 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_L:
|
||||||
|
if (hasCtrl) {
|
||||||
|
controller.cycleMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
case Qt.Key_N:
|
case Qt.Key_N:
|
||||||
if (hasCtrl) {
|
if (hasCtrl) {
|
||||||
controller.selectNextSection();
|
controller.selectNextSection();
|
||||||
@@ -200,13 +235,19 @@ FocusScope {
|
|||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Tab:
|
case Qt.Key_Tab:
|
||||||
if (actionPanel.hasActions) {
|
if (hasCtrl && actionPanel.hasActions) {
|
||||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
controller.selectNext();
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Backtab:
|
case Qt.Key_Backtab:
|
||||||
if (actionPanel.expanded)
|
if (hasCtrl && actionPanel.expanded) {
|
||||||
actionPanel.hide();
|
const reverse = true;
|
||||||
|
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.selectPrevious();
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
@@ -270,7 +311,7 @@ FocusScope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !editMode
|
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: footerBar
|
id: footerBar
|
||||||
@@ -388,7 +429,7 @@ FocusScope {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Tab " + I18n.tr("actions")
|
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: actionPanel.hasActions
|
visible: actionPanel.hasActions
|
||||||
@@ -548,7 +589,6 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -697,8 +737,6 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
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)
|
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 {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -731,6 +769,7 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
function onSearchQueryRequested(query) {
|
function onSearchQueryRequested(query) {
|
||||||
searchField.text = query;
|
searchField.text = query;
|
||||||
|
searchField.cursorPosition = query.length;
|
||||||
}
|
}
|
||||||
function onModeChanged() {
|
function onModeChanged() {
|
||||||
extFilterField.text = "";
|
extFilterField.text = "";
|
||||||
@@ -941,6 +980,15 @@ FocusScope {
|
|||||||
keyNavigationBacktab: editEnvVarsField
|
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
|
height: 24
|
||||||
z: 100
|
z: 100
|
||||||
visible: {
|
visible: {
|
||||||
|
if (BlurService.enabled)
|
||||||
|
return false;
|
||||||
if (mainListView.contentHeight <= mainListView.height)
|
if (mainListView.contentHeight <= mainListView.height)
|
||||||
return false;
|
return false;
|
||||||
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
||||||
@@ -449,7 +451,7 @@ Item {
|
|||||||
case "apps":
|
case "apps":
|
||||||
return "apps";
|
return "apps";
|
||||||
default:
|
default:
|
||||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
return "search_off";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,9 +487,9 @@ Item {
|
|||||||
case "plugins":
|
case "plugins":
|
||||||
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||||
case "apps":
|
case "apps":
|
||||||
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
return I18n.tr("No apps found");
|
||||||
default:
|
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 item = items[i]
|
||||||
var itemScore
|
var itemScore
|
||||||
|
|
||||||
if (query && item._preScored !== undefined) {
|
if (item._preScored !== undefined && (query || item._preScored > 900)) {
|
||||||
itemScore = item._preScored
|
itemScore = item._preScored
|
||||||
} else {
|
} else {
|
||||||
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
||||||
|
|||||||
@@ -75,6 +75,50 @@ StyledRect {
|
|||||||
return determineFileType(fileName) === "image";
|
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) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
if (lowerName.startsWith("dockerfile")) {
|
if (lowerName.startsWith("dockerfile")) {
|
||||||
@@ -124,7 +168,11 @@ StyledRect {
|
|||||||
property string imagePath: {
|
property string imagePath: {
|
||||||
if (weMode && delegateRoot.fileIsDir)
|
if (weMode && delegateRoot.fileIsDir)
|
||||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
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('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
@@ -149,7 +197,7 @@ StyledRect {
|
|||||||
source: gridPreviewImage
|
source: gridPreviewImage
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
maskSource: gridImageMask
|
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
|
maskThresholdMin: 0.5
|
||||||
maskSpreadAtMin: 1
|
maskSpreadAtMin: 1
|
||||||
}
|
}
|
||||||
@@ -175,7 +223,7 @@ StyledRect {
|
|||||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||||
size: iconSizes[iconSizeIndex] * 0.45
|
size: iconSizes[iconSizeIndex] * 0.45
|
||||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
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";
|
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) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
if (lowerName.startsWith("dockerfile")) {
|
if (lowerName.startsWith("dockerfile")) {
|
||||||
@@ -127,7 +167,13 @@ StyledRect {
|
|||||||
Image {
|
Image {
|
||||||
id: listPreviewImage
|
id: listPreviewImage
|
||||||
anchors.fill: parent
|
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('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: 32
|
sourceSize.width: 32
|
||||||
@@ -141,7 +187,7 @@ StyledRect {
|
|||||||
source: listPreviewImage
|
source: listPreviewImage
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
maskSource: listImageMask
|
maskSource: listImageMask
|
||||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
|
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
|
||||||
maskThresholdMin: 0.5
|
maskThresholdMin: 0.5
|
||||||
maskSpreadAtMin: 1
|
maskSpreadAtMin: 1
|
||||||
}
|
}
|
||||||
@@ -166,7 +212,7 @@ StyledRect {
|
|||||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
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 {
|
StyledText {
|
||||||
Layout.alignment: Qt.AlignLeft
|
Layout.alignment: Qt.AlignLeft
|
||||||
text: KeybindsService.cheatsheet.title || "Keybinds"
|
text: KeybindsService.cheatsheet.title || i18n("Keybinds")
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -309,10 +309,12 @@ DankModal {
|
|||||||
id: keyText
|
id: keyText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: Theme.secondary
|
color: Theme.secondary
|
||||||
text: modelData.key || ""
|
text: (modelData.key || "").replace(/\+/g, " + ")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
isMonospace: true
|
isMonospace: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: Math.min(implicitWidth, 148)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +327,7 @@ DankModal {
|
|||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
opacity: 0.9
|
opacity: 0.9
|
||||||
elide: Text.ElideRight
|
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 {
|
StyledText {
|
||||||
text: `Details for "${networkSSID}"`
|
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -102,7 +102,7 @@ DankModal {
|
|||||||
id: detailsText
|
id: detailsText
|
||||||
|
|
||||||
width: parent.width
|
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
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: `Details for "${networkID}"`
|
text: I18n.tr("Details for \"%1\"").arg(networkSSID)
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -102,7 +102,7 @@ DankModal {
|
|||||||
id: detailsText
|
id: detailsText
|
||||||
|
|
||||||
width: parent.width
|
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
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|||||||
@@ -503,5 +503,35 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
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"),
|
"text": I18n.tr("Widgets"),
|
||||||
"icon": "widgets",
|
"icon": "widgets",
|
||||||
"tabIndex": 22
|
"tabIndex": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frame",
|
||||||
|
"text": I18n.tr("Frame"),
|
||||||
|
"icon": "frame_source",
|
||||||
|
"tabIndex": 33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -156,7 +162,7 @@ Rectangle {
|
|||||||
{
|
{
|
||||||
"id": "running_apps",
|
"id": "running_apps",
|
||||||
"text": I18n.tr("Running Apps"),
|
"text": I18n.tr("Running Apps"),
|
||||||
"icon": "apps",
|
"icon": "app_registration",
|
||||||
"tabIndex": 19,
|
"tabIndex": 19,
|
||||||
"hyprlandNiriOnly": true
|
"hyprlandNiriOnly": true
|
||||||
},
|
},
|
||||||
@@ -237,7 +243,7 @@ Rectangle {
|
|||||||
{
|
{
|
||||||
"id": "system",
|
"id": "system",
|
||||||
"text": I18n.tr("System"),
|
"text": I18n.tr("System"),
|
||||||
"icon": "computer",
|
"icon": "memory",
|
||||||
"collapsedByDefault": true,
|
"collapsedByDefault": true,
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
@@ -266,6 +272,12 @@ Rectangle {
|
|||||||
"tabIndex": 8,
|
"tabIndex": 8,
|
||||||
"cupsOnly": true
|
"cupsOnly": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "multiplexers",
|
||||||
|
"text": I18n.tr("Multiplexers"),
|
||||||
|
"icon": "terminal",
|
||||||
|
"tabIndex": 32
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "window_rules",
|
"id": "window_rules",
|
||||||
"text": I18n.tr("Window Rules"),
|
"text": I18n.tr("Window Rules"),
|
||||||
@@ -364,6 +376,7 @@ Rectangle {
|
|||||||
if (_collapsedIds.indexOf(marker) < 0)
|
if (_collapsedIds.indexOf(marker) < 0)
|
||||||
_collapsedIds = _collapsedIds + id + ",";
|
_collapsedIds = _collapsedIds + id + ",";
|
||||||
}
|
}
|
||||||
|
SessionData.setSettingsSidebarState(_expandedIds, _collapsedIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _setAutoExpanded(id, value) {
|
function _setAutoExpanded(id, value) {
|
||||||
@@ -532,6 +545,11 @@ Rectangle {
|
|||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
root._expandedIds = SessionData.settingsSidebarExpandedIds;
|
||||||
|
root._collapsedIds = SessionData.settingsSidebarCollapsedIds;
|
||||||
|
}
|
||||||
|
|
||||||
StyledTextMetrics {
|
StyledTextMetrics {
|
||||||
id: __m1
|
id: __m1
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ DankPopout {
|
|||||||
if (!lc)
|
if (!lc)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const query = _pendingQuery;
|
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
|
||||||
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
||||||
_pendingMode = "";
|
_pendingMode = "";
|
||||||
_pendingQuery = "";
|
_pendingQuery = "";
|
||||||
@@ -102,12 +102,9 @@ DankPopout {
|
|||||||
if (lc.controller) {
|
if (lc.controller) {
|
||||||
lc.controller.searchMode = mode;
|
lc.controller.searchMode = mode;
|
||||||
lc.controller.pluginFilter = "";
|
lc.controller.pluginFilter = "";
|
||||||
lc.controller.searchQuery = "";
|
lc.controller.searchQuery = query;
|
||||||
if (query) {
|
|
||||||
lc.controller.setSearchQuery(query);
|
lc.controller.performSearch();
|
||||||
} else {
|
|
||||||
lc.controller.performSearch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lc.resetScroll?.();
|
lc.resetScroll?.();
|
||||||
lc.actionPanel?.hide();
|
lc.actionPanel?.hide();
|
||||||
@@ -136,7 +133,7 @@ DankPopout {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
property bool isClosing: false
|
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
appDrawerPopout.close();
|
appDrawerPopout.close();
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Variants {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
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;
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +100,30 @@ Variants {
|
|||||||
Connections {
|
Connections {
|
||||||
target: currentWallpaper
|
target: currentWallpaper
|
||||||
function onStatusChanged() {
|
function onStatusChanged() {
|
||||||
if (currentWallpaper.status === Image.Ready) {
|
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||||
root._renderSettling = true;
|
return;
|
||||||
renderSettleTimer.restart();
|
root._renderSettling = true;
|
||||||
}
|
renderSettleTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
transitionAnimation.stop();
|
||||||
root.transitionProgress = 0;
|
root.transitionProgress = 0;
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
currentWallpaper.source = nextWallpaper.source;
|
currentWallpaper.source = nextWallpaper.source;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
}
|
}
|
||||||
@@ -175,6 +197,9 @@ Variants {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
|
|
||||||
nextWallpaper.source = newPath;
|
nextWallpaper.source = newPath;
|
||||||
|
|
||||||
if (nextWallpaper.status === Image.Ready)
|
if (nextWallpaper.status === Image.Ready)
|
||||||
@@ -201,6 +226,7 @@ Variants {
|
|||||||
visible: false
|
visible: false
|
||||||
opacity: 1
|
opacity: 1
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -213,6 +239,7 @@ Variants {
|
|||||||
visible: false
|
visible: false
|
||||||
opacity: 0
|
opacity: 0
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
retainWhileLoading: true
|
||||||
smooth: true
|
smooth: true
|
||||||
cache: true
|
cache: true
|
||||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||||
@@ -295,6 +322,8 @@ Variants {
|
|||||||
root.useNextForEffect = false;
|
root.useNextForEffect = false;
|
||||||
nextWallpaper.source = "";
|
nextWallpaper.source = "";
|
||||||
root.transitionProgress = 0.0;
|
root.transitionProgress = 0.0;
|
||||||
|
root._renderSettling = true;
|
||||||
|
renderSettleTimer.restart();
|
||||||
root.effectActive = false;
|
root.effectActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ PluginComponent {
|
|||||||
id: detailRoot
|
id: detailRoot
|
||||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -252,7 +252,7 @@ PluginComponent {
|
|||||||
width: parent ? parent.width : 300
|
width: parent ? parent.width : 300
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHighest
|
color: Theme.surfaceLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
opacity: 1.0
|
opacity: 1.0
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Row {
|
|||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Theme.surfaceContainer
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
border.color: Theme.primarySelected
|
border.color: Theme.primarySelected
|
||||||
border.width: 0
|
border.width: 0
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ DankPopout {
|
|||||||
|
|
||||||
backgroundInteractive: !anyModalOpen
|
backgroundInteractive: !anyModalOpen
|
||||||
|
|
||||||
|
onCredentialsPromptOpenChanged: {
|
||||||
|
if (credentialsPromptOpen && shouldBeVisible)
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPolkitModalOpenChanged: {
|
||||||
|
if (polkitModalOpen && shouldBeVisible)
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
customKeyboardFocus: {
|
customKeyboardFocus: {
|
||||||
if (!shouldBeVisible)
|
if (!shouldBeVisible)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
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)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -218,9 +218,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
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)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
id: deviceRipple
|
id: deviceRipple
|
||||||
@@ -397,9 +397,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -129,8 +129,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -164,8 +165,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ Item {
|
|||||||
width: 320
|
width: 320
|
||||||
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
opacity: modalVisible ? 1 : 0
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
@@ -243,8 +242,8 @@ Rectangle {
|
|||||||
if (isConnecting)
|
if (isConnecting)
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||||
if (deviceMouseArea.containsMouse)
|
if (deviceMouseArea.containsMouse)
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
return Theme.primaryHoverLight;
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
|
return Theme.surfaceLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
border.color: {
|
border.color: {
|
||||||
@@ -252,8 +251,9 @@ Rectangle {
|
|||||||
return Theme.warning;
|
return Theme.warning;
|
||||||
if (isConnected)
|
if (isConnected)
|
||||||
return Theme.primary;
|
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 {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -490,9 +490,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
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)
|
color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Theme.outlineLight
|
||||||
border.width: 0
|
border.width: 1
|
||||||
opacity: isInteractive ? 1 : 0.6
|
opacity: isInteractive ? 1 : 0.6
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
|
||||||
border.width: modelData.mount === currentMountPath ? 2 : 0
|
border.width: modelData.mount === currentMountPath ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -308,9 +308,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
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)
|
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Theme.primary
|
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: isActive ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wiredContentRow
|
id: wiredContentRow
|
||||||
@@ -565,9 +565,9 @@ Rectangle {
|
|||||||
width: wifiContent.width
|
width: wifiContent.width
|
||||||
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
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)
|
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: wifiDelegate.isConnected ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wifiContentRow
|
id: wifiContentRow
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Item {
|
|||||||
required property var axis
|
required property var axis
|
||||||
required property var barConfig
|
required property var barConfig
|
||||||
|
|
||||||
|
visible: !SettingsData.frameEnabled
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -37,6 +39,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property real rt: {
|
property real rt: {
|
||||||
|
if (SettingsData.frameEnabled)
|
||||||
|
return SettingsData.frameRounding;
|
||||||
if (barConfig?.squareCorners ?? false)
|
if (barConfig?.squareCorners ?? false)
|
||||||
return 0;
|
return 0;
|
||||||
if (barWindow.hasMaximizedToplevel)
|
if (barWindow.hasMaximizedToplevel)
|
||||||
@@ -255,11 +259,12 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${cr} 0`;
|
let d = `M ${crE} 0`;
|
||||||
d += ` L ${w - cr} 0`;
|
d += ` L ${w - crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w} ${h + r}`;
|
d += ` L ${w} ${h + r}`;
|
||||||
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
||||||
@@ -273,9 +278,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
||||||
}
|
}
|
||||||
d += ` L 0 ${cr}`;
|
d += ` L 0 ${crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -285,11 +290,12 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${cr} ${fullH}`;
|
let d = `M ${crE} ${fullH}`;
|
||||||
d += ` L ${w - cr} ${fullH}`;
|
d += ` L ${w - crE} ${fullH}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w} 0`;
|
d += ` L ${w} 0`;
|
||||||
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
||||||
@@ -303,9 +309,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||||
}
|
}
|
||||||
d += ` L 0 ${fullH - cr}`;
|
d += ` L 0 ${fullH - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -314,11 +320,12 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M 0 ${cr}`;
|
let d = `M 0 ${crE}`;
|
||||||
d += ` L 0 ${h - cr}`;
|
d += ` L 0 ${h - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w + r} ${h}`;
|
d += ` L ${w + r} ${h}`;
|
||||||
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
||||||
@@ -332,9 +339,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
||||||
}
|
}
|
||||||
d += ` L ${cr} 0`;
|
d += ` L ${crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -344,11 +351,12 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${fullW} ${cr}`;
|
let d = `M ${fullW} ${crE}`;
|
||||||
d += ` L ${fullW} ${h - cr}`;
|
d += ` L ${fullW} ${h - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L 0 ${h}`;
|
d += ` L 0 ${h}`;
|
||||||
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
||||||
@@ -362,9 +370,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||||
}
|
}
|
||||||
d += ` L ${fullW - cr} 0`;
|
d += ` L ${fullW - crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -357,6 +358,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === centerRepeater.count - 1
|
isLast: index === centerRepeater.count - 1
|
||||||
sectionSpacing: parent.itemSpacing
|
sectionSpacing: parent.itemSpacing
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Item {
|
|||||||
required property var rootWindow
|
required property var rootWindow
|
||||||
required property var barConfig
|
required property var barConfig
|
||||||
|
|
||||||
|
readonly property var blurBarWindow: barWindow
|
||||||
|
|
||||||
property var leftWidgetsModel
|
property var leftWidgetsModel
|
||||||
property var centerWidgetsModel
|
property var centerWidgetsModel
|
||||||
property var rightWidgetsModel
|
property var rightWidgetsModel
|
||||||
@@ -21,6 +23,31 @@ Item {
|
|||||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
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 hLeftSection: hLeftSection
|
||||||
property alias hCenterSection: hCenterSection
|
property alias hCenterSection: hCenterSection
|
||||||
property alias hRightSection: hRightSection
|
property alias hRightSection: hRightSection
|
||||||
@@ -29,10 +56,14 @@ Item {
|
|||||||
property alias vRightSection: vRightSection
|
property alias vRightSection: vRightSection
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
|
||||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
|
||||||
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
|
anchors.topMargin: (barWindow.isVertical
|
||||||
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
|
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
|
||||||
|
: 0) + _frameTopInset
|
||||||
|
anchors.bottomMargin: (barWindow.isVertical
|
||||||
|
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
|
||||||
|
: 0) + _frameBottomInset
|
||||||
clip: false
|
clip: false
|
||||||
|
|
||||||
property int componentMapRevision: 0
|
property int componentMapRevision: 0
|
||||||
@@ -408,6 +439,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hLeftSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
RightSection {
|
RightSection {
|
||||||
id: hRightSection
|
id: hRightSection
|
||||||
@@ -434,6 +471,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hRightSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
CenterSection {
|
CenterSection {
|
||||||
id: hCenterSection
|
id: hCenterSection
|
||||||
@@ -460,6 +503,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: hCenterSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -493,6 +542,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: vLeftSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
CenterSection {
|
CenterSection {
|
||||||
id: vCenterSection
|
id: vCenterSection
|
||||||
@@ -520,6 +575,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
Binding {
|
||||||
|
target: vCenterSection
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: topBarContent.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
RightSection {
|
RightSection {
|
||||||
id: vRightSection
|
id: vRightSection
|
||||||
@@ -548,6 +609,12 @@ Item {
|
|||||||
value: topBarContent.barConfig
|
value: topBarContent.barConfig
|
||||||
restoreMode: Binding.RestoreNone
|
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.layer: dBarLayer
|
||||||
WlrLayershell.namespace: "dms:bar"
|
WlrLayershell.namespace: "dms:bar"
|
||||||
|
|
||||||
@@ -132,7 +248,9 @@ PanelWindow {
|
|||||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||||
readonly property string _barId: barConfig?.id ?? "default"
|
readonly property string _barId: barConfig?.id ?? "default"
|
||||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
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() {
|
function _updateBackgroundAlpha() {
|
||||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||||
@@ -157,6 +275,7 @@ PanelWindow {
|
|||||||
property string screenName: modelData.name
|
property string screenName: modelData.name
|
||||||
|
|
||||||
property bool hasMaximizedToplevel: false
|
property bool hasMaximizedToplevel: false
|
||||||
|
property bool hasFullscreenToplevel: false
|
||||||
property bool shouldHideForWindows: false
|
property bool shouldHideForWindows: false
|
||||||
|
|
||||||
function _updateHasMaximizedToplevel() {
|
function _updateHasMaximizedToplevel() {
|
||||||
@@ -179,6 +298,25 @@ PanelWindow {
|
|||||||
hasMaximizedToplevel = false;
|
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() {
|
function _updateShouldHideForWindows() {
|
||||||
if (!(barConfig?.showOnWindowsOpen ?? false)) {
|
if (!(barConfig?.showOnWindowsOpen ?? false)) {
|
||||||
shouldHideForWindows = false;
|
shouldHideForWindows = false;
|
||||||
@@ -258,7 +396,7 @@ PanelWindow {
|
|||||||
shouldHideForWindows = filtered.length > 0;
|
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 {
|
Behavior on effectiveSpacing {
|
||||||
enabled: barWindow.visible
|
enabled: barWindow.visible
|
||||||
@@ -269,7 +407,12 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property int notificationCount: NotificationService.notifications.length
|
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 real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||||
|
|
||||||
readonly property bool hasAdjacentTopBar: {
|
readonly property bool hasAdjacentTopBar: {
|
||||||
@@ -289,7 +432,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -312,7 +455,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -336,7 +479,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -360,7 +503,7 @@ PanelWindow {
|
|||||||
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
|
||||||
if (!onThisScreen)
|
if (!onThisScreen)
|
||||||
return false;
|
return false;
|
||||||
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name)
|
if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -485,6 +628,7 @@ PanelWindow {
|
|||||||
target: CompositorService
|
target: CompositorService
|
||||||
function onToplevelsChanged() {
|
function onToplevelsChanged() {
|
||||||
barWindow._updateHasMaximizedToplevel();
|
barWindow._updateHasMaximizedToplevel();
|
||||||
|
barWindow._updateHasFullscreenToplevel();
|
||||||
barWindow._updateShouldHideForWindows();
|
barWindow._updateShouldHideForWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -493,6 +637,7 @@ PanelWindow {
|
|||||||
target: NiriService
|
target: NiriService
|
||||||
function onAllWorkspacesChanged() {
|
function onAllWorkspacesChanged() {
|
||||||
barWindow._updateHasMaximizedToplevel();
|
barWindow._updateHasMaximizedToplevel();
|
||||||
|
barWindow._updateHasFullscreenToplevel();
|
||||||
barWindow._updateShouldHideForWindows();
|
barWindow._updateShouldHideForWindows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -523,7 +668,7 @@ PanelWindow {
|
|||||||
|
|
||||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
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 effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||||
|
|
||||||
@@ -664,10 +809,13 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool reveal: {
|
property bool reveal: {
|
||||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
|
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||||
if (inOverviewWithShow)
|
if (inOverviewWithShow)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (barWindow.hasFullscreenToplevel)
|
||||||
|
return false;
|
||||||
|
|
||||||
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
|
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
|
||||||
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
|
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
|
||||||
if (barWindow.shouldHideForWindows)
|
if (barWindow.shouldHideForWindows)
|
||||||
@@ -686,6 +834,8 @@ PanelWindow {
|
|||||||
onHasActivePopoutChanged: evaluateReveal()
|
onHasActivePopoutChanged: evaluateReveal()
|
||||||
|
|
||||||
function updateActivePopoutState() {
|
function updateActivePopoutState() {
|
||||||
|
if (!barWindow.screen)
|
||||||
|
return;
|
||||||
const screenName = barWindow.screen.name;
|
const screenName = barWindow.screen.name;
|
||||||
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
|
||||||
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
|
||||||
@@ -756,7 +906,7 @@ PanelWindow {
|
|||||||
top: barWindow.isVertical ? parent.top : undefined
|
top: barWindow.isVertical ? parent.top : undefined
|
||||||
bottom: barWindow.isVertical ? parent.bottom : 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
|
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === rowRepeater.count - 1
|
isLast: index === rowRepeater.count - 1
|
||||||
sectionSpacing: parent.rowSpacing
|
sectionSpacing: parent.rowSpacing
|
||||||
@@ -103,6 +105,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === columnRepeater.count - 1
|
isLast: index === columnRepeater.count - 1
|
||||||
sectionSpacing: parent.columnSpacing
|
sectionSpacing: parent.columnSpacing
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ DankPopout {
|
|||||||
|
|
||||||
function setProfile(profile) {
|
function setProfile(profile) {
|
||||||
if (typeof PowerProfiles === "undefined") {
|
if (typeof PowerProfiles === "undefined") {
|
||||||
ToastService.showError("power-profiles-daemon not available");
|
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PowerProfiles.profile = profile;
|
PowerProfiles.profile = profile;
|
||||||
if (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
|
width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2
|
||||||
readonly property string timeInfoText: {
|
readonly property string timeInfoText: {
|
||||||
if (!BatteryService.batteryAvailable)
|
if (!BatteryService.batteryAvailable)
|
||||||
return "Power profile management available";
|
return I18n.tr("Power profile management available");
|
||||||
const time = BatteryService.formatTimeRemaining();
|
const time = BatteryService.formatTimeRemaining();
|
||||||
if (time !== "Unknown") {
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ DankPopout {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power"
|
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : I18n.tr("Power")
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: {
|
color: {
|
||||||
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
|
||||||
@@ -338,7 +338,7 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
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
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Bold
|
font.weight: Font.Bold
|
||||||
@@ -393,7 +393,7 @@ DankPopout {
|
|||||||
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
|
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.model || `Battery ${index + 1}`
|
text: modelData.model || I18n.tr("Battery %1").arg(index + 1)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Item {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool overrideAxisLayout: false
|
property bool overrideAxisLayout: false
|
||||||
property bool forceVerticalLayout: false
|
property bool forceVerticalLayout: false
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === rowRepeater.count - 1
|
isLast: index === rowRepeater.count - 1
|
||||||
sectionSpacing: parent.rowSpacing
|
sectionSpacing: parent.rowSpacing
|
||||||
@@ -105,6 +107,7 @@ Item {
|
|||||||
barThickness: root.barThickness
|
barThickness: root.barThickness
|
||||||
barSpacing: root.barSpacing
|
barSpacing: root.barSpacing
|
||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
|
blurBarWindow: root.blurBarWindow
|
||||||
isFirst: index === 0
|
isFirst: index === 0
|
||||||
isLast: index === columnRepeater.count - 1
|
isLast: index === columnRepeater.count - 1
|
||||||
sectionSpacing: parent.columnSpacing
|
sectionSpacing: parent.columnSpacing
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Loader {
|
|||||||
property real barThickness: 48
|
property real barThickness: 48
|
||||||
property real barSpacing: 4
|
property real barSpacing: 4
|
||||||
property var barConfig: null
|
property var barConfig: null
|
||||||
|
property var blurBarWindow: null
|
||||||
property bool isFirst: false
|
property bool isFirst: false
|
||||||
property bool isLast: false
|
property bool isLast: false
|
||||||
property real sectionSpacing: 0
|
property real sectionSpacing: 0
|
||||||
@@ -92,6 +93,14 @@ Loader {
|
|||||||
restoreMode: Binding.RestoreNone
|
restoreMode: Binding.RestoreNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: root.item
|
||||||
|
when: root.item && "blurBarWindow" in root.item
|
||||||
|
property: "blurBarWindow"
|
||||||
|
value: root.blurBarWindow
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: root.item
|
target: root.item
|
||||||
when: root.item && "axis" in root.item
|
when: root.item && "axis" in root.item
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ BasePill {
|
|||||||
let appId = Paths.moddedAppId(rawAppId);
|
let appId = Paths.moddedAppId(rawAppId);
|
||||||
|
|
||||||
let coreAppData = null;
|
let coreAppData = null;
|
||||||
if (rawAppId === "org.quickshell") {
|
if (rawAppId === "org.quickshell" || rawAppId === "com.danklinux.dms") {
|
||||||
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||||
if (coreAppData) {
|
if (coreAppData) {
|
||||||
appId = coreAppData.builtInPluginId;
|
appId = coreAppData.builtInPluginId;
|
||||||
@@ -630,7 +630,7 @@ BasePill {
|
|||||||
if (appItem.isFocused && colorizeEnabled) {
|
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.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
|
border.width: dragHandler.dragging ? 2 : 0
|
||||||
@@ -697,7 +697,7 @@ BasePill {
|
|||||||
mipmap: true
|
mipmap: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready && !coreIcon.visible
|
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.smooth: true
|
||||||
layer.mipmap: true
|
layer.mipmap: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
@@ -990,7 +990,7 @@ BasePill {
|
|||||||
break;
|
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 moddedId = Paths.moddedAppId(modelData.appId);
|
||||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ PanelWindow {
|
|||||||
property int margin: 10
|
property int margin: 10
|
||||||
property bool hidePin: false
|
property bool hidePin: false
|
||||||
property var desktopEntry: null
|
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 bool isVertical: false
|
||||||
property string edge: "top"
|
property string edge: "top"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Quickshell
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modules.Plugins
|
import qs.Modules.Plugins
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
BasePill {
|
BasePill {
|
||||||
@@ -93,6 +94,15 @@ BasePill {
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: contextMenuWindow
|
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"
|
WlrLayershell.namespace: "dms:clipboard-context-menu"
|
||||||
|
|
||||||
property bool isVertical: false
|
property bool isVertical: false
|
||||||
@@ -187,8 +197,8 @@ BasePill {
|
|||||||
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
|
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 1
|
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||||
|
|
||||||
opacity: contextMenuWindow.visible ? 1 : 0
|
opacity: contextMenuWindow.visible ? 1 : 0
|
||||||
visible: opacity > 0
|
visible: opacity > 0
|
||||||
@@ -224,7 +234,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -264,7 +274,7 @@ BasePill {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 30
|
height: 30
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
@@ -38,12 +39,20 @@ BasePill {
|
|||||||
property var _vAudio: null
|
property var _vAudio: null
|
||||||
property var _vBrightness: null
|
property var _vBrightness: null
|
||||||
property var _vMic: 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) {
|
onWheel: function (wheelEvent) {
|
||||||
const delta = wheelEvent.angleDelta.y;
|
const delta = wheelEvent.angleDelta.y;
|
||||||
if (delta === 0)
|
if (delta === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
|
||||||
const rootX = wheelEvent.x - root.leftMargin;
|
const rootX = wheelEvent.x - root.leftMargin;
|
||||||
const rootY = wheelEvent.y - root.topMargin;
|
const rootY = wheelEvent.y - root.topMargin;
|
||||||
|
|
||||||
@@ -72,6 +81,8 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRightClicked: function (rootX, rootY) {
|
onRightClicked: function (rootX, rootY) {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
|
||||||
if (root.isVerticalOrientation && _vCol) {
|
if (root.isVerticalOrientation && _vCol) {
|
||||||
const pos = root.mapToItem(_vCol, rootX, rootY);
|
const pos = root.mapToItem(_vCol, rootX, rootY);
|
||||||
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
|
||||||
@@ -279,26 +290,142 @@ BasePill {
|
|||||||
return CupsService.getTotalJobsNum() > 0;
|
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() {
|
function hasNoVisibleIcons() {
|
||||||
if (root.showScreenSharingIcon && NiriService.hasCasts)
|
return !root.controlCenterRenderModel.some(entry => entry.visible);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
@@ -309,12 +436,7 @@ BasePill {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
root._hRow = controlIndicators;
|
root._hRow = controlIndicators;
|
||||||
root._vCol = controlColumn;
|
root._vCol = controlColumn;
|
||||||
root._hAudio = audioIcon.parent;
|
root.clearInteractionRefs();
|
||||||
root._hBrightness = brightnessIcon.parent;
|
|
||||||
root._hMic = micIcon.parent;
|
|
||||||
root._vAudio = audioIconV.parent;
|
|
||||||
root._vBrightness = brightnessIconV.parent;
|
|
||||||
root._vMic = micIconV.parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -324,162 +446,151 @@ BasePill {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
Item {
|
Repeater {
|
||||||
width: parent.width
|
model: root.controlCenterRenderModel
|
||||||
height: root.vIconSize
|
Item {
|
||||||
visible: root.showScreenSharingIcon && NiriService.hasCasts
|
id: verticalGroupItem
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
property string interactionGroupId: modelData.id
|
||||||
|
|
||||||
DankIcon {
|
width: parent.width
|
||||||
name: "screen_record"
|
height: {
|
||||||
size: root.vIconSize
|
switch (modelData.id) {
|
||||||
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
|
case "audio":
|
||||||
anchors.centerIn: parent
|
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 {
|
Component.onCompleted: {
|
||||||
width: parent.width
|
root.registerInteractionDelegate(true, verticalGroupItem);
|
||||||
height: root.vIconSize
|
root.refreshInteractionRefs();
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
}
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (root) {
|
||||||
|
root.unregisterInteractionDelegate(verticalGroupItem);
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: root.refreshInteractionRefs()
|
||||||
|
onInteractionGroupIdChanged: {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.getNetworkIconName()
|
anchors.centerIn: parent
|
||||||
size: root.vIconSize
|
visible: !verticalGroupItem.modelData.composite
|
||||||
color: root.getNetworkIconColor()
|
name: {
|
||||||
anchors.centerIn: parent
|
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 {
|
DankIcon {
|
||||||
width: parent.width
|
id: audioIconV
|
||||||
height: root.vIconSize
|
visible: verticalGroupItem.modelData.id === "audio"
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
name: root.getVolumeIconName()
|
||||||
|
size: root.vIconSize
|
||||||
|
color: Theme.widgetIconColor
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
NumericText {
|
||||||
name: "vpn_lock"
|
id: audioPercentV
|
||||||
size: root.vIconSize
|
visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
|
||||||
anchors.centerIn: parent
|
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 {
|
DankIcon {
|
||||||
width: parent.width
|
id: micIconV
|
||||||
height: root.vIconSize
|
visible: verticalGroupItem.modelData.id === "microphone"
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
name: root.getMicIconName()
|
||||||
|
size: root.vIconSize
|
||||||
|
color: root.getMicIconColor()
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
NumericText {
|
||||||
name: "bluetooth"
|
id: micPercentV
|
||||||
size: root.vIconSize
|
visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
|
||||||
anchors.centerIn: parent
|
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 {
|
DankIcon {
|
||||||
width: parent.width
|
id: brightnessIconV
|
||||||
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0)
|
visible: verticalGroupItem.modelData.id === "brightness"
|
||||||
visible: root.showAudioIcon
|
name: root.getBrightnessIconName()
|
||||||
|
size: root.vIconSize
|
||||||
|
color: Theme.widgetIconColor
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
NumericText {
|
||||||
id: audioIconV
|
id: brightnessPercentV
|
||||||
name: root.getVolumeIconName()
|
visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
|
||||||
size: root.vIconSize
|
text: Math.round(getBrightness() * 100) + "%"
|
||||||
color: Theme.widgetIconColor
|
reserveText: "100%"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
anchors.top: parent.top
|
color: Theme.widgetTextColor
|
||||||
}
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: brightnessIconV.bottom
|
||||||
NumericText {
|
anchors.topMargin: 2
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,157 +614,206 @@ BasePill {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
DankIcon {
|
Repeater {
|
||||||
name: "screen_record"
|
model: root.controlCenterRenderModel
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
Item {
|
||||||
id: networkIcon
|
id: horizontalGroupItem
|
||||||
name: root.getNetworkIconName()
|
required property var modelData
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
required property int index
|
||||||
color: root.getNetworkIconColor()
|
property string interactionGroupId: modelData.id
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.showNetworkIcon && NetworkService.networkAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
width: {
|
||||||
id: vpnIcon
|
switch (modelData.id) {
|
||||||
name: "vpn_lock"
|
case "audio":
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
return audioGroup.width;
|
||||||
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
|
case "microphone":
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
return micGroup.width;
|
||||||
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
|
case "brightness":
|
||||||
}
|
return brightnessGroup.width;
|
||||||
|
default:
|
||||||
|
return root.getControlCenterIconSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implicitWidth: width
|
||||||
|
height: root.widgetThickness - root.horizontalPadding * 2
|
||||||
|
visible: modelData.visible
|
||||||
|
|
||||||
DankIcon {
|
Component.onCompleted: {
|
||||||
id: bluetoothIcon
|
root.registerInteractionDelegate(false, horizontalGroupItem);
|
||||||
name: "bluetooth"
|
root.refreshInteractionRefs();
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
}
|
||||||
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
|
Component.onDestruction: {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
if (root) {
|
||||||
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
|
root.unregisterInteractionDelegate(horizontalGroupItem);
|
||||||
}
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVisibleChanged: root.refreshInteractionRefs()
|
||||||
|
onInteractionGroupIdChanged: {
|
||||||
|
root.refreshInteractionRefs();
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
DankIcon {
|
||||||
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.reservedWidth : 0) + 4
|
id: iconOnlyItem
|
||||||
implicitWidth: width
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
height: root.widgetThickness - root.horizontalPadding * 2
|
anchors.left: parent.left
|
||||||
color: "transparent"
|
visible: !horizontalGroupItem.modelData.composite
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
name: {
|
||||||
visible: root.showAudioIcon
|
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 {
|
Rectangle {
|
||||||
id: audioIcon
|
id: audioGroup
|
||||||
name: root.getVolumeIconName()
|
width: audioContent.implicitWidth + 2
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
implicitWidth: width
|
||||||
color: Theme.widgetIconColor
|
height: parent.height
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
color: "transparent"
|
||||||
anchors.left: parent.left
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: 2
|
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 {
|
DankIcon {
|
||||||
name: "settings"
|
name: "settings"
|
||||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
size: root.getControlCenterIconSize()
|
||||||
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
color: root.isActive ? Theme.primary : Theme.widgetIconColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: root.hasNoVisibleIcons()
|
visible: root.hasNoVisibleIcons()
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
|
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 (CompositorService.isHyprland) {
|
||||||
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
|
if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return activeWindow && activeWindow.title;
|
return activeWindow && (activeWindow.title || activeWindow.appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
|
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
|
||||||
@@ -145,7 +145,7 @@ BasePill {
|
|||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
asynchronous: 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.smooth: true
|
||||||
layer.mipmap: true
|
layer.mipmap: true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
@@ -212,17 +212,19 @@ BasePill {
|
|||||||
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
|
||||||
const appName = appText.text;
|
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;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title || !appName) {
|
if (!title || !appName)
|
||||||
return title;
|
return title;
|
||||||
}
|
|
||||||
|
|
||||||
if (title.endsWith(appName)) {
|
if (title.endsWith(appName))
|
||||||
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ BasePill {
|
|||||||
height: 20
|
height: 20
|
||||||
radius: 10
|
radius: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
visible: root.playerAvailable
|
visible: root.playerAvailable
|
||||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ BasePill {
|
|||||||
height: 20
|
height: 20
|
||||||
radius: 10
|
radius: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||||
visible: playerAvailable
|
visible: playerAvailable
|
||||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user