mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 18:42:06 -04:00
Compare commits
91 Commits
31aeb8dc4b
...
hotfix-1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf41ed5ca | ||
|
|
5033bdc630 | ||
|
|
b8bfaf9a26 | ||
|
|
da45714c54 | ||
|
|
0c2d00b79c | ||
|
|
c10b42f599 | ||
|
|
4c617cf022 | ||
|
|
e75b95b854 | ||
|
|
6b15670918 | ||
|
|
c52b9e19a1 | ||
|
|
7a3444bd30 | ||
|
|
a733d760e4 | ||
|
|
1b33079e39 | ||
|
|
1cf0dd1031 | ||
|
|
8d49a5cbfc | ||
|
|
f5928b09d3 | ||
|
|
38373aa5f2 | ||
|
|
665680e15e | ||
|
|
210607cfbc | ||
|
|
69fca14611 | ||
|
|
10a235e686 | ||
|
|
253cc7f8a3 | ||
|
|
a63ad99684 | ||
|
|
c44c032879 | ||
|
|
dc1ce55971 | ||
|
|
9f65882a12 | ||
|
|
97bf83cce6 | ||
|
|
96bf0162d6 | ||
|
|
73b833731a | ||
|
|
84522aeaad | ||
|
|
faf1a277d2 | ||
|
|
60515736e6 | ||
|
|
1715e2eab7 | ||
|
|
4e14cf5cce | ||
|
|
a644c93b1b | ||
|
|
f9428a1009 | ||
|
|
b4b51785e5 | ||
|
|
0a97df6d49 | ||
|
|
352ba77677 | ||
|
|
d320035d97 | ||
|
|
8d262a9555 | ||
|
|
9bfa8310d2 | ||
|
|
088ed806ae | ||
|
|
07d2c94676 | ||
|
|
0bc1b7a3c2 | ||
|
|
c5987b28c0 | ||
|
|
18901c7cde | ||
|
|
519a8357a1 | ||
|
|
799773c62b | ||
|
|
247a674c79 | ||
|
|
72b598057c | ||
|
|
8180e30e8e | ||
|
|
dd9851b4f0 | ||
|
|
056576fbe7 | ||
|
|
d28a5bdf7f | ||
|
|
f62ea119f7 | ||
|
|
a1f9b98727 | ||
|
|
b9c8914d46 | ||
|
|
8697840d46 | ||
|
|
1f64bb8031 | ||
|
|
eea7d12c0b | ||
|
|
85173126f4 | ||
|
|
222187d8a6 | ||
|
|
bef3f65f63 | ||
|
|
bff83fe563 | ||
|
|
cbf00d133a | ||
|
|
347f06b758 | ||
|
|
9070903512 | ||
|
|
e9d030f6d8 | ||
|
|
fbf9e6d1b9 | ||
|
|
e803812344 | ||
|
|
9a64f2acf0 | ||
|
|
c647eafadc | ||
|
|
720ec07d13 | ||
|
|
4b4334e611 | ||
|
|
b69a96e80b | ||
|
|
1e6a73fd60 | ||
|
|
60b6280750 | ||
|
|
9e079f8a4b | ||
|
|
62c2e858ef | ||
|
|
78357d45bb | ||
|
|
3ff9564c9b | ||
|
|
b0989cecad | ||
|
|
47be6a1033 | ||
|
|
31b415b086 | ||
|
|
7156e1e299 | ||
|
|
c72c9bfb08 | ||
|
|
73c75fcc2c | ||
|
|
2ff42eba41 | ||
|
|
9f13465cd7 | ||
|
|
366a98e0cc |
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,6 +236,7 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
defer ddc.Close()
|
defer ddc.Close()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
|
ddc.WaitPending()
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"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"
|
||||||
@@ -82,7 +83,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
@@ -90,6 +91,7 @@ var (
|
|||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
|
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -468,6 +470,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
|
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -500,7 +503,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -509,9 +512,24 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results = append(results, checkCompositorBlurSupport())
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCompositorBlurSupport() checkResult {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusInfo, "Unable to verify", err.Error(), doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if supported {
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusOK, "Supported", "Compositor supports ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil && len(output) == 0 {
|
||||||
@@ -535,6 +553,8 @@ func detectRunningWM() string {
|
|||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
return "niri"
|
return "niri"
|
||||||
|
case os.Getenv("MIRACLESOCK") != "":
|
||||||
|
return "Miracle WM"
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
}
|
}
|
||||||
@@ -553,6 +573,7 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -561,6 +582,7 @@ ShellRoot {
|
|||||||
property bool idleMonitorAvailable: false
|
property bool idleMonitorAvailable: false
|
||||||
property bool idleInhibitorAvailable: false
|
property bool idleInhibitorAvailable: false
|
||||||
property bool shortcutInhibitorAvailable: false
|
property bool shortcutInhibitorAvailable: false
|
||||||
|
property bool backgroundBlurAvailable: false
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 50
|
interval: 50
|
||||||
@@ -578,16 +600,18 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
var testItem = Qt.createQmlObject(
|
||||||
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
'import Quickshell; import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
||||||
|
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
||||||
'}',
|
'}',
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
|
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
||||||
testItem.destroy()
|
testItem.destroy()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -596,6 +620,8 @@ ShellRoot {
|
|||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||||
|
|
||||||
|
console.warn(root.backgroundBlurAvailable ? "FEATURE:BackgroundBlur:OK" : "FEATURE:BackgroundBlur:UNAVAILABLE")
|
||||||
|
|
||||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,6 +642,7 @@ ShellRoot {
|
|||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
|
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -820,10 +847,14 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
return !utils.CommandExists(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(terminals) > 0 {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
|||||||
@@ -109,16 +109,41 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
if isArchPackageInstalled("dms-shell-bin") {
|
var isAUR bool
|
||||||
packageName = "dms-shell-bin"
|
if isArchPackageInstalled("dms-shell") {
|
||||||
|
packageName = "dms-shell"
|
||||||
} else if isArchPackageInstalled("dms-shell-git") {
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
packageName = "dms-shell-git"
|
packageName = "dms-shell-git"
|
||||||
|
isAUR = true
|
||||||
|
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||||
|
packageName = "dms-shell-bin"
|
||||||
|
isAUR = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
fmt.Println("Info: No dms-shell package found.")
|
||||||
fmt.Println("Info: Falling back to git-based update method...")
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
return updateOtherDistros()
|
return updateOtherDistros()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isAUR {
|
||||||
|
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||||
|
if !confirmUpdate() {
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
|
||||||
|
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dms successfully updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
|||||||
271
core/cmd/dms/immutable_policy.go
Normal file
271
core/cmd/dms/immutable_policy.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||||
|
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
immutablePolicyOnce sync.Once
|
||||||
|
immutablePolicy immutableCommandPolicy
|
||||||
|
immutablePolicyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/cli-policy.default.json
|
||||||
|
var defaultCLIPolicyJSON []byte
|
||||||
|
|
||||||
|
type immutableCommandPolicy struct {
|
||||||
|
ImmutableSystem bool
|
||||||
|
ImmutableReason string
|
||||||
|
BlockedCommands []string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliPolicyFile struct {
|
||||||
|
PolicyVersion int `json:"policy_version"`
|
||||||
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCommandSpec(raw string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||||
|
return strings.Join(strings.Fields(normalized), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBlockedCommands(raw []string) []string {
|
||||||
|
normalized := make([]string, 0, len(raw))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, cmd := range raw {
|
||||||
|
spec := normalizeCommandSpec(cmd)
|
||||||
|
if spec == "" || seen[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[spec] = true
|
||||||
|
normalized = append(normalized, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||||
|
normalizedPath := normalizeCommandSpec(commandPath)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range blocked {
|
||||||
|
spec := normalizeCommandSpec(entry)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(data, &policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||||
|
policyFile, err := loadPolicyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policyFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyFile.ImmutableSystem != nil {
|
||||||
|
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands != nil {
|
||||||
|
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
}
|
||||||
|
if policyFile.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*policyFile.Message)
|
||||||
|
if msg != "" {
|
||||||
|
base.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSReleaseMap(path string) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||||
|
values[key] = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyToken(text string, tokens ...string) bool {
|
||||||
|
if text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.Contains(text, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectImmutableSystem() (bool, string) {
|
||||||
|
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||||
|
return true, "/run/ostree-booted is present"
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := readOSReleaseMap("/etc/os-release")
|
||||||
|
if len(osRelease) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := osRelease["ID"]
|
||||||
|
idLike := osRelease["ID_LIKE"]
|
||||||
|
variantID := osRelease["VARIANT_ID"]
|
||||||
|
name := osRelease["NAME"]
|
||||||
|
prettyName := osRelease["PRETTY_NAME"]
|
||||||
|
|
||||||
|
immutableIDs := map[string]bool{
|
||||||
|
"bluefin": true,
|
||||||
|
"bazzite": true,
|
||||||
|
"silverblue": true,
|
||||||
|
"kinoite": true,
|
||||||
|
"sericea": true,
|
||||||
|
"onyx": true,
|
||||||
|
"aurora": true,
|
||||||
|
"fedora-iot": true,
|
||||||
|
"fedora-coreos": true,
|
||||||
|
}
|
||||||
|
if immutableIDs[id] {
|
||||||
|
return true, "os-release ID=" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||||
|
if hasAnyToken(variantID, markers...) {
|
||||||
|
return true, "os-release VARIANT_ID=" + variantID
|
||||||
|
}
|
||||||
|
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||||
|
return true, "os-release ID_LIKE=" + idLike
|
||||||
|
}
|
||||||
|
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||||
|
return true, "os-release identifies an atomic/ostree variant"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||||
|
immutablePolicyOnce.Do(func() {
|
||||||
|
detectedImmutable, reason := detectImmutableSystem()
|
||||||
|
immutablePolicy = immutableCommandPolicy{
|
||||||
|
ImmutableSystem: detectedImmutable,
|
||||||
|
ImmutableReason: reason,
|
||||||
|
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||||
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPolicy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||||
|
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if defaultPolicy.BlockedCommands != nil {
|
||||||
|
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||||
|
}
|
||||||
|
if defaultPolicy.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||||
|
if msg != "" {
|
||||||
|
immutablePolicy.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if immutablePolicyErr != nil {
|
||||||
|
return nil, immutablePolicyErr
|
||||||
|
}
|
||||||
|
return &immutablePolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||||
|
policy, err := getImmutablePolicy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !policy.ImmutableSystem {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||||
|
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := ""
|
||||||
|
if policy.ImmutableReason != "" {
|
||||||
|
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,19 +17,10 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to update
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
@@ -37,7 +29,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,36 +5,30 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Block root
|
clipboard.MaybeServeAndExit()
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -616,6 +616,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 +664,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
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||||
path, err := exec.LookPath(cmd)
|
// safe to run as root (e.g. shell completion, help).
|
||||||
if err != nil {
|
func isReadOnlyCommand(args []string) bool {
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
for _, arg := range args[1:] {
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch arg {
|
||||||
|
case "completion", "help", "__complete":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return path, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,100 +12,166 @@ import (
|
|||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
|
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||||
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
|
func MaybeServeAndExit() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := os.Getenv(envMime)
|
||||||
|
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||||
|
cachePath := os.Getenv(envCacheFile)
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cachePath != "":
|
||||||
|
data, err = os.ReadFile(cachePath)
|
||||||
|
os.Remove(cachePath)
|
||||||
|
default:
|
||||||
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
return serveClipboard(data, mimeType, pasteOnce)
|
||||||
total := 0
|
|
||||||
for total < len(data) {
|
|
||||||
n, err := writer.Write(data[total:])
|
|
||||||
total += n
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if total != len(data) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, mimeType, pasteOnce)
|
|
||||||
}
|
}
|
||||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if !foreground {
|
if foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServeReader(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
cmd := exec.Command(os.Args[0])
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
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(),
|
||||||
if stdinSource, ok := data.(*os.File); ok {
|
envServe+"=1",
|
||||||
cmd.Stdin = stdinSource
|
envMime+"="+mimeType,
|
||||||
return cmd.Start()
|
)
|
||||||
|
if pasteOnce {
|
||||||
|
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||||
}
|
}
|
||||||
|
cmd.Env = append(cmd.Env, extra...)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
func waitReady(cmd *exec.Cmd) error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
var buf [1]byte
|
||||||
if _, err := io.Copy(stdin, data); err != nil {
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
stdin.Close()
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
}
|
||||||
if err := stdin.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close stdin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
cachedData, err := createClipboardCacheFile()
|
cacheFile, err := createClipboardCacheFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create clipboard cache file: %w", err)
|
return fmt.Errorf("create cache file: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(cachedData.Name())
|
cachePath := cacheFile.Name()
|
||||||
|
|
||||||
if _, err := io.Copy(cachedData, data); err != nil {
|
if _, err := cacheFile.Write(data); err != nil {
|
||||||
return fmt.Errorf("cache clipboard data: %w", err)
|
cacheFile.Close()
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("write cache file: %w", err)
|
||||||
}
|
}
|
||||||
if err := cachedData.Close(); err != nil {
|
if err := cacheFile.Close(); err != nil {
|
||||||
return fmt.Errorf("close temp cache file: %w", err)
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("close cache file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return copyServeWithWriter(func(writer io.Writer) error {
|
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||||
cachedFile, err := os.Open(cachedData.Name())
|
cmd.Stdin = nil
|
||||||
|
if err := waitReady(cmd); err != nil {
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
|
|
||||||
|
switch src := data.(type) {
|
||||||
|
case *os.File:
|
||||||
|
cmd.Stdin = src
|
||||||
|
return waitReady(cmd)
|
||||||
|
|
||||||
|
default:
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open temp cache file: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
defer cachedFile.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(writer, cachedFile); err != nil {
|
stdout, err := cmd.StdoutPipe()
|
||||||
return fmt.Errorf("write clipboard data: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, mimeType, pasteOnce)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalReady() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte{1})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClipboardCacheFile() (*os.File, error) {
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
@@ -129,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
return os.CreateTemp("", "dms-clipboard-*")
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -171,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -215,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
sendErr := make(chan error, 1)
|
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err := writeTo(file); err != nil {
|
_, _ = file.Write(data)
|
||||||
select {
|
|
||||||
case sendErr <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -242,13 +299,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
case err := <-sendErr:
|
|
||||||
return err
|
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
@@ -502,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -535,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
file.Write(data)
|
_, _ = file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
|||||||
|
|
||||||
# === Sizing & Layout ===
|
# === Sizing & Layout ===
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100%
|
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
|||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
|
|
||||||
|
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
|
match app-id=r#"^org\.gnome\.Calculator$"#
|
||||||
match app-id=r#"^gnome-calculator$"#
|
match app-id=r#"^gnome-calculator$"#
|
||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
@@ -252,6 +253,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))
|
||||||
}
|
}
|
||||||
@@ -206,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.packageInstalled("dms-shell-bin") {
|
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -292,6 +324,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
|
||||||
|
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
|
||||||
|
}
|
||||||
|
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: System Packages
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -409,6 +448,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if a.packageInstalled("quickshell-git") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.packageInstalled("quickshell") {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Removing stable quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||||
|
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||||
|
}
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: "Building quickshell-git before system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
||||||
|
}
|
||||||
|
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -417,6 +487,9 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||||
|
if slices.Contains(packages, "dms-shell") {
|
||||||
|
args = append(args, "--assume-installed", "dms-shell-compositor=1")
|
||||||
|
}
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -504,7 +577,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
var dmsShell []string
|
var dmsShell []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -524,6 +597,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)
|
||||||
@@ -575,7 +658,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -598,51 +681,65 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
}
|
}
|
||||||
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
if pkg == "dms-shell-bin" {
|
{
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,42 +768,9 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "sudo pacman -U built-package",
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
|
||||||
var files []string
|
var files []string
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
// For DMS split packages, install base package
|
files = matches
|
||||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err == nil {
|
|
||||||
for _, match := range matches {
|
|
||||||
basename := filepath.Base(match)
|
|
||||||
// Always include base package
|
|
||||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
|
||||||
files = append(files, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also update compositor-specific packages if they're installed
|
|
||||||
if strings.HasSuffix(pkg, "-git") {
|
|
||||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
|
||||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
|
||||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
|
||||||
files = append(files, hyprlandMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if a.packageInstalled("dms-shell-niri-git") {
|
|
||||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
|
||||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
|
||||||
files = append(files, niriMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For other packages, install all built packages
|
|
||||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
|
||||||
files = matches
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return fmt.Errorf("no package files found after building %s", pkg)
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
|
|||||||
@@ -91,9 +91,25 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
}
|
||||||
return err == nil
|
|
||||||
|
func debianPackageInstalledPrecisely(pkg string) bool {
|
||||||
|
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)) == "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(values []string, target string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func debianRepoArchitecture(arch string) string {
|
func debianRepoArchitecture(arch string) string {
|
||||||
@@ -204,12 +220,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -389,6 +405,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -492,20 +516,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
groupIndex := 0
|
||||||
Phase: PhaseSystemPackages,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
Progress: 0.40,
|
if len(groupPackages) == 0 {
|
||||||
Step: "Installing system packages...",
|
return nil
|
||||||
IsComplete: false,
|
}
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
groupIndex++
|
||||||
|
startProgress := 0.40
|
||||||
|
endProgress := 0.60
|
||||||
|
if totalGroups > 1 {
|
||||||
|
if groupIndex == 1 {
|
||||||
|
endProgress = 0.50
|
||||||
|
} else {
|
||||||
|
startProgress = 0.50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := d.aptInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: startProgress,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -484,28 +484,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -515,26 +494,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
for _, pkg := range packages {
|
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
if minimal {
|
||||||
break
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := f.dnfInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.70,
|
|
||||||
Step: "Installing COPR packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
core/internal/distros/minimal_install.go
Normal file
44
core/internal/distros/minimal_install.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
type minimalInstallGroup struct {
|
||||||
|
packages []string
|
||||||
|
minimal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldPreferMinimalInstall(pkg string) bool {
|
||||||
|
switch pkg {
|
||||||
|
case "niri", "niri-git":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if shouldPreferMinimalInstall(pkg) {
|
||||||
|
minimal = append(minimal, pkg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normal = append(normal, pkg)
|
||||||
|
}
|
||||||
|
return normal, minimal
|
||||||
|
}
|
||||||
|
|
||||||
|
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
||||||
|
normal, minimal := splitMinimalInstallPackages(packages)
|
||||||
|
groups := make([]minimalInstallGroup, 0, 2)
|
||||||
|
if len(minimal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: minimal,
|
||||||
|
minimal: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(normal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: normal,
|
||||||
|
minimal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -199,35 +201,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{}
|
||||||
"make",
|
|
||||||
"unzip",
|
|
||||||
"gcc",
|
|
||||||
"gcc-c++",
|
|
||||||
"cmake",
|
|
||||||
"ninja",
|
|
||||||
"pkgconf-pkg-config",
|
|
||||||
"git",
|
|
||||||
"qt6-base-devel",
|
|
||||||
"qt6-declarative-devel",
|
|
||||||
"qt6-declarative-private-devel",
|
|
||||||
"qt6-shadertools",
|
|
||||||
"qt6-shadertools-devel",
|
|
||||||
"qt6-wayland-devel",
|
|
||||||
"qt6-waylandclient-private-devel",
|
|
||||||
"spirv-tools-devel",
|
|
||||||
"cli11-devel",
|
|
||||||
"wayland-protocols-devel",
|
|
||||||
"libgbm-devel",
|
|
||||||
"libdrm-devel",
|
|
||||||
"pipewire-devel",
|
|
||||||
"jemalloc-devel",
|
|
||||||
"wayland-utils",
|
|
||||||
"Mesa-libGLESv3-devel",
|
|
||||||
"pam-devel",
|
|
||||||
"glib2-devel",
|
|
||||||
"polkit-devel",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -297,6 +271,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -327,7 +305,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,7 +320,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,9 +410,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
||||||
|
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{openSUSENiriWaylandServerPackage}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||||
|
for _, pkg := range extraPkgs {
|
||||||
|
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
||||||
|
systemPkgs = append(systemPkgs, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
names := make([]string, len(packages))
|
names := make([]string, len(packages))
|
||||||
for i, pkg := range packages {
|
for i, pkg := range packages {
|
||||||
@@ -514,27 +515,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||||
|
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
||||||
|
|
||||||
|
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "hd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "iso:/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseZypperInstallMediaAliases(output string) []string {
|
||||||
|
var aliases []string
|
||||||
|
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || !strings.Contains(line, "|") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, "|")
|
||||||
|
if len(parts) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := parts[1]
|
||||||
|
enabled := strings.ToLower(parts[3])
|
||||||
|
uri := parts[len(parts)-1]
|
||||||
|
|
||||||
|
if alias == "" || strings.EqualFold(alias, "alias") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enabled != "" && enabled != "yes" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isOpenSUSEInstallMediaURI(uri) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases = append(aliases, alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
||||||
|
output, err := listCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
||||||
|
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases := parseZypperInstallMediaAliases(string(output))
|
||||||
|
if len(aliases) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.055,
|
||||||
|
Step: "Disabling install media repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range aliases {
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
||||||
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
||||||
|
}
|
||||||
|
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"zypper", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
groupIndex := 0
|
||||||
Phase: PhaseSystemPackages,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
Progress: 0.40,
|
if len(groupPackages) == 0 {
|
||||||
Step: "Installing system packages...",
|
return nil
|
||||||
IsComplete: false,
|
}
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := o.zypperInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -100,9 +100,7 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -454,21 +452,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
args := []string{"apt-get", "install", "-y"}
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -477,21 +461,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
args = append(args, packages...)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
Phase: PhaseAURPackages,
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
Progress: 0.70,
|
totalGroups := len(groups)
|
||||||
Step: "Installing PPA packages...",
|
|
||||||
IsComplete: false,
|
groupIndex := 0
|
||||||
NeedsSudo: true,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := u.aptInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal file
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# AppArmor profile for dms-greeter
|
||||||
|
#
|
||||||
|
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
||||||
|
# Manual edits will be overwritten on next sync.
|
||||||
|
#
|
||||||
|
# Mode: complain (denials are logged, nothing is blocked)
|
||||||
|
# To switch to enforce after validating with `aa-logprof`:
|
||||||
|
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
||||||
|
#
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
#include <abstractions/bash>
|
||||||
|
|
||||||
|
# The launcher script itself
|
||||||
|
/usr/bin/dms-greeter r,
|
||||||
|
|
||||||
|
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
||||||
|
/var/cache/dms-greeter/ rw,
|
||||||
|
/var/cache/dms-greeter/** rwlk,
|
||||||
|
|
||||||
|
# DMS config — packaged path
|
||||||
|
/usr/share/quickshell/dms-greeter/ r,
|
||||||
|
/usr/share/quickshell/dms-greeter/** r,
|
||||||
|
/usr/share/quickshell/ r,
|
||||||
|
/usr/share/quickshell/** r,
|
||||||
|
|
||||||
|
# DMS config — system and user overrides
|
||||||
|
/etc/dms/ r,
|
||||||
|
/etc/dms/** r,
|
||||||
|
/usr/share/dms/ r,
|
||||||
|
/usr/share/dms/** r,
|
||||||
|
/home/*/.config/quickshell/ r,
|
||||||
|
/home/*/.config/quickshell/** r,
|
||||||
|
/root/.config/quickshell/ r,
|
||||||
|
/root/.config/quickshell/** r,
|
||||||
|
|
||||||
|
# greetd / PAM — read-only for session setup
|
||||||
|
/etc/greetd/ r,
|
||||||
|
/etc/greetd/** r,
|
||||||
|
/etc/pam.d/ r,
|
||||||
|
/etc/pam.d/** r,
|
||||||
|
/usr/lib/pam.d/ r,
|
||||||
|
/usr/lib/pam.d/** r,
|
||||||
|
|
||||||
|
# Compositor binaries — run unconfined so each compositor uses its own profile
|
||||||
|
/usr/bin/niri Ux,
|
||||||
|
/usr/bin/hyprland Ux,
|
||||||
|
/usr/bin/Hyprland Ux,
|
||||||
|
/usr/bin/sway Ux,
|
||||||
|
/usr/bin/labwc Ux,
|
||||||
|
/usr/bin/scroll Ux,
|
||||||
|
/usr/bin/miracle-wm Ux,
|
||||||
|
/usr/bin/mango Ux,
|
||||||
|
|
||||||
|
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
||||||
|
/usr/bin/qs Ux,
|
||||||
|
/usr/bin/quickshell Ux,
|
||||||
|
|
||||||
|
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
||||||
|
/run/user/[0-9]*/ rw,
|
||||||
|
/run/user/[0-9]*/** rw,
|
||||||
|
|
||||||
|
# DRM / GPU devices (required for Wayland compositor startup)
|
||||||
|
/dev/dri/ r,
|
||||||
|
/dev/dri/* rw,
|
||||||
|
/dev/udmabuf rw,
|
||||||
|
|
||||||
|
# Input devices
|
||||||
|
/dev/input/ r,
|
||||||
|
/dev/input/* r,
|
||||||
|
|
||||||
|
# Systemd journal / logging
|
||||||
|
/run/systemd/journal/socket rw,
|
||||||
|
/dev/log rw,
|
||||||
|
|
||||||
|
# Shell helper binaries invoked by the launcher script
|
||||||
|
/usr/bin/env ix,
|
||||||
|
/usr/bin/mkdir ix,
|
||||||
|
/usr/bin/cat ix,
|
||||||
|
/usr/bin/grep ix,
|
||||||
|
/usr/bin/dirname ix,
|
||||||
|
/usr/bin/basename ix,
|
||||||
|
/usr/bin/command ix,
|
||||||
|
/bin/env ix,
|
||||||
|
/bin/mkdir ix,
|
||||||
|
|
||||||
|
# Signal management (compositor lifecycle)
|
||||||
|
signal (send, receive) set=("term", "int", "hup", "kill"),
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
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 writeTestJSON(t *testing.T, path string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingsJSON string
|
||||||
|
sessionJSON string
|
||||||
|
wantSourcePath string
|
||||||
|
wantResolvedWallpaper string
|
||||||
|
wantDynamicOverrideUsed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||||
|
"matugenScheme": "scheme-tonal-spot",
|
||||||
|
"iconTheme": "Papirus"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":true}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||||
|
wantDynamicOverrideUsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic theme without override uses desktop colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": ""
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "purple",
|
||||||
|
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||||
|
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||||
|
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||||
|
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||||
|
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||||
|
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/sblinch/kdl-go"
|
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,7 +291,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
parser.currentSource = overridePath
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,103 @@ type NiriParser struct {
|
|||||||
conflictingConfigs map[string]*NiriKeyBinding
|
conflictingConfigs map[string]*NiriKeyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseKDL(data []byte) (*document.Document, error) {
|
||||||
|
return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKDLBraces(input string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(input))
|
||||||
|
|
||||||
|
var prev byte
|
||||||
|
n := len(input)
|
||||||
|
for i := 0; i < n; {
|
||||||
|
c := input[i]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '"':
|
||||||
|
end := findStringEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '"'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '/':
|
||||||
|
end := findLineCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '\n'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '*':
|
||||||
|
end := findBlockCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '/'
|
||||||
|
i = end
|
||||||
|
case c == '{' && prev != 0 && !isBraceAdjacentSpace(prev):
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStringEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
for i := start + 1; i < n; {
|
||||||
|
switch s[i] {
|
||||||
|
case '\\':
|
||||||
|
i += 2
|
||||||
|
case '"':
|
||||||
|
return i + 1
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLineCommentEnd(s string, start int) int {
|
||||||
|
for i := start + 2; i < len(s); i++ {
|
||||||
|
if s[i] == '\n' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBlockCommentEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
depth := 1
|
||||||
|
for i := start + 2; i < n && depth > 0; {
|
||||||
|
switch {
|
||||||
|
case i+1 < n && s[i] == '/' && s[i+1] == '*':
|
||||||
|
depth++
|
||||||
|
i += 2
|
||||||
|
case i+1 < n && s[i] == '*' && s[i+1] == '/':
|
||||||
|
depth--
|
||||||
|
i += 2
|
||||||
|
if depth == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBraceAdjacentSpace(b byte) bool {
|
||||||
|
switch b {
|
||||||
|
case ' ', '\t', '\n', '\r', '{':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
@@ -91,7 +188,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,7 +256,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,74 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
||||||
|
config := `recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
Alt+grave { next-window filter="app-id"; }
|
||||||
|
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||||
|
Alt+Escape { next-window scope="all"; }
|
||||||
|
Alt+Shift+Escape{ previous-window scope="all"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed on valid niri config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var found *NiriKeyBinding
|
||||||
|
for i := range result.Section.Keybinds {
|
||||||
|
kb := &result.Section.Keybinds[i]
|
||||||
|
if kb.Key == "Escape" && slices.Contains(kb.Mods, "Alt") && slices.Contains(kb.Mods, "Shift") {
|
||||||
|
found = kb
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
t.Fatal("Alt+Shift+Escape bind missing — '{' without preceding space was not handled")
|
||||||
|
}
|
||||||
|
if found.Action != "previous-window" {
|
||||||
|
t.Errorf("Action = %q, want %q", found.Action, "previous-window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeKDLBraces(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"already spaced", "node { child }\n", "node { child }\n"},
|
||||||
|
{"missing space", "node{ child }\n", "node { child }\n"},
|
||||||
|
{"niri keybind", "Alt+Shift+Escape{ previous-window; }", "Alt+Shift+Escape { previous-window; }"},
|
||||||
|
{"brace inside string", `node "a{b" { child }`, `node "a{b" { child }`},
|
||||||
|
{"brace in line comment", "// foo{bar\nnode { }", "// foo{bar\nnode { }"},
|
||||||
|
{"brace in block comment", "/* foo{bar */ node{ }", "/* foo{bar */ node { }"},
|
||||||
|
{"escaped quote in string", `node "a\"b{c" { }`, `node "a\"b{c" { }`},
|
||||||
|
{"leading brace", "{ child }", "{ child }"},
|
||||||
|
{"nested missing space", "a{b{ c }}", "a {b { c }}"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := normalizeKDLBraces(tc.in)
|
||||||
|
if got != tc.out {
|
||||||
|
t.Errorf("normalizeKDLBraces(%q) = %q, want %q", tc.in, got, tc.out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriParseKeyCombo(t *testing.T) {
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
combo string
|
combo string
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ type Options struct {
|
|||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -274,6 +275,10 @@ func buildOnce(opts *Options) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -331,6 +336,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -597,10 +606,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -298,22 +299,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 +321,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 +398,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 +427,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
|
||||||
|
|||||||
@@ -215,31 +215,34 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if timer, exists := b.debounceTimers[id]; exists {
|
if existing, exists := b.debounceTimers[id]; exists {
|
||||||
timer.Reset(200 * time.Millisecond)
|
if existing.Stop() {
|
||||||
} else {
|
b.debounceWg.Done()
|
||||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
}
|
||||||
b.debounceMutex.Lock()
|
|
||||||
pending, exists := b.debouncePending[id]
|
|
||||||
if exists {
|
|
||||||
delete(b.debouncePending, id)
|
|
||||||
}
|
|
||||||
b.debounceMutex.Unlock()
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.setBrightnessImmediateWithExponent(id, pending.percent)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pending.callback != nil {
|
|
||||||
pending.callback()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.debounceWg.Add(1)
|
||||||
|
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||||
|
defer b.debounceWg.Done()
|
||||||
|
|
||||||
|
b.debounceMutex.Lock()
|
||||||
|
pending, hasPending := b.debouncePending[id]
|
||||||
|
delete(b.debouncePending, id)
|
||||||
|
delete(b.debounceTimers, id)
|
||||||
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
|
if !hasPending {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.setBrightnessImmediateWithExponent(id, pending.percent); err != nil {
|
||||||
|
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending.callback != nil {
|
||||||
|
pending.callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
b.debounceMutex.Unlock()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -490,5 +493,19 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
|||||||
return percent
|
return percent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) WaitPending() {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
b.debounceWg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Debug("WaitPending timed out waiting for DDC writes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) Close() {
|
func (b *DDCBackend) Close() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type DDCBackend struct {
|
|||||||
debounceMutex sync.Mutex
|
debounceMutex sync.Mutex
|
||||||
debounceTimers map[string]*time.Timer
|
debounceTimers map[string]*time.Timer
|
||||||
debouncePending map[string]ddcPendingSet
|
debouncePending map[string]ddcPendingSet
|
||||||
|
debounceWg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type ddcPendingSet struct {
|
type ddcPendingSet struct {
|
||||||
|
|||||||
@@ -212,9 +212,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var offer any
|
var offer any
|
||||||
if e.Id != nil {
|
switch {
|
||||||
|
case e.Id != nil:
|
||||||
offer = e.Id
|
offer = e.Id
|
||||||
} else if e.OfferId != 0 {
|
case e.OfferId != 0:
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
offer = m.offerRegistry[e.OfferId]
|
offer = m.offerRegistry[e.OfferId]
|
||||||
m.offerMutex.RUnlock()
|
m.offerMutex.RUnlock()
|
||||||
@@ -224,10 +225,6 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
wasOwner := m.isOwner
|
wasOwner := m.isOwner
|
||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
|
|
||||||
if offer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if wasOwner {
|
if wasOwner {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,9 +233,11 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
m.currentOffer = offer
|
m.currentOffer = offer
|
||||||
|
|
||||||
if prevOffer != nil && prevOffer != offer {
|
if prevOffer != nil && prevOffer != offer {
|
||||||
m.offerMutex.Lock()
|
m.releaseOffer(prevOffer)
|
||||||
delete(m.offerMimeTypes, prevOffer)
|
}
|
||||||
m.offerMutex.Unlock()
|
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
@@ -292,6 +291,33 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
log.Info("Data device setup complete")
|
log.Info("Data device setup complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) releaseOffer(offer any) {
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
typedOffer, ok := offer.(*ext_data_control.ExtDataControlOfferV1)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.offerMutex.Lock()
|
||||||
|
delete(m.offerMimeTypes, offer)
|
||||||
|
delete(m.offerRegistry, typedOffer.ID())
|
||||||
|
m.offerMutex.Unlock()
|
||||||
|
typedOffer.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) releaseCurrentSource() {
|
||||||
|
if m.currentSource == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source, ok := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||||
|
m.currentSource = nil
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
@@ -395,7 +421,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|||||||
if extractHash(v) != hash {
|
if extractHash(v) != hash {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -413,7 +439,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
var count int
|
var count int
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -456,6 +482,14 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeEntry(data []byte) (Entry, error) {
|
func decodeEntry(data []byte) (Entry, error) {
|
||||||
|
return decodeEntryFields(data, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEntryMeta(data []byte) (Entry, error) {
|
||||||
|
return decodeEntryFields(data, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEntryFields(data []byte, withData bool) (Entry, error) {
|
||||||
buf := bytes.NewReader(data)
|
buf := bytes.NewReader(data)
|
||||||
var e Entry
|
var e Entry
|
||||||
|
|
||||||
@@ -463,8 +497,15 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
|
|
||||||
var dataLen uint32
|
var dataLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||||
e.Data = make([]byte, dataLen)
|
switch {
|
||||||
buf.Read(e.Data)
|
case withData:
|
||||||
|
e.Data = make([]byte, dataLen)
|
||||||
|
buf.Read(e.Data)
|
||||||
|
default:
|
||||||
|
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mimeLen uint32
|
var mimeLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||||
@@ -668,14 +709,9 @@ func sizeStr(size int) string {
|
|||||||
func (m *Manager) updateState() {
|
func (m *Manager) updateState() {
|
||||||
history := m.GetHistory()
|
history := m.GetHistory()
|
||||||
|
|
||||||
for i := range history {
|
|
||||||
history[i].Data = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var current *Entry
|
var current *Entry
|
||||||
if len(history) > 0 {
|
if len(history) > 0 {
|
||||||
c := history[0]
|
c := history[0]
|
||||||
c.Data = nil
|
|
||||||
current = &c
|
current = &c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,7 +786,7 @@ func (m *Manager) GetHistory() []Entry {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
|
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -935,7 +971,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -958,7 +994,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
if b != nil {
|
if b != nil {
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, _ := decodeEntry(v)
|
entry, _ := decodeEntryMeta(v)
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1102,7 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.releaseCurrentSource()
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
m.sourceMutex.Lock()
|
m.sourceMutex.Lock()
|
||||||
m.sourceMimeTypes = []string{mimeType}
|
m.sourceMimeTypes = []string{mimeType}
|
||||||
@@ -1145,9 +1182,11 @@ func (m *Manager) Close() {
|
|||||||
m.subscribers = make(map[string]chan State)
|
m.subscribers = make(map[string]chan State)
|
||||||
m.subMutex.Unlock()
|
m.subMutex.Unlock()
|
||||||
|
|
||||||
if m.currentSource != nil {
|
m.releaseCurrentSource()
|
||||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
source.Destroy()
|
if m.currentOffer != nil {
|
||||||
|
m.releaseOffer(m.currentOffer)
|
||||||
|
m.currentOffer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.dataDevice != nil {
|
if m.dataDevice != nil {
|
||||||
@@ -1191,11 +1230,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Skip pinned entries
|
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1310,7 +1348,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1335,7 +1373,6 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Data = nil
|
|
||||||
all = append(all, entry)
|
all = append(all, entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1510,7 +1547,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1528,7 +1565,6 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check pinned count
|
|
||||||
cfg := m.getConfig()
|
cfg := m.getConfig()
|
||||||
pinnedCount := 0
|
pinnedCount := 0
|
||||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
@@ -1538,7 +1574,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1629,12 +1665,11 @@ func (m *Manager) GetPinnedEntries() []Entry {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
entry.Data = nil
|
|
||||||
pinned = append(pinned, entry)
|
pinned = append(pinned, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1660,7 +1695,7 @@ func (m *Manager) GetPinnedCount() int {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -1779,6 +1814,7 @@ func (m *Manager) CopyFile(filePath string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.releaseCurrentSource()
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
m.ownerLock.Lock()
|
||||||
|
|||||||
@@ -158,18 +158,26 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID && bssid == currentBSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID && bssid == currentBSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,19 +463,27 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Hidden: hiddenSSIDs[ssid],
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,19 +1019,27 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := connected && apSSID == ssid
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: apSSID,
|
SSID: apSSID,
|
||||||
BSSID: apBSSID,
|
BSSID: apBSSID,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: connected && apSSID == ssid,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
Hidden: hiddenSSIDs[apSSID],
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Device: name,
|
Device: name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||||
|
t.Skipf("system D-Bus unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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),
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
if isGit {
|
if isGit {
|
||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell-bin"
|
return "dms-shell"
|
||||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||||
if isGit {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -10,7 +9,7 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = [
|
packages = [
|
||||||
dmsPkgs.dms-shell
|
cfg.package
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||||
++ lib.optionals cfg.enableVPN [
|
++ lib.optionals cfg.enableVPN [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
cfg = config.programs.dank-material-shell.greeter;
|
||||||
|
cfgDms = config.programs.dank-material-shell;
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
@@ -23,19 +24,20 @@ 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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
lib.escapeShellArgs (
|
lib.escapeShellArgs (
|
||||||
[
|
[
|
||||||
"sh"
|
"sh"
|
||||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||||
"--cache-dir"
|
"--cache-dir"
|
||||||
cacheDir
|
cacheDir
|
||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
"${cfg.package}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
@@ -65,6 +67,21 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell.greeter = {
|
options.programs.dank-material-shell.greeter = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
if config.programs.dank-material-shell.enable
|
||||||
|
then config.programs.dank-material-shell.package
|
||||||
|
else built from source;
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
The DankMaterialShell package to use for the greeter.
|
||||||
|
|
||||||
|
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
||||||
|
otherwise defaults to building from source.
|
||||||
|
'';
|
||||||
|
};
|
||||||
compositor.name = lib.mkOption {
|
compositor.name = lib.mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"niri"
|
"niri"
|
||||||
@@ -179,7 +196,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,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -13,7 +12,6 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
dmsPkgs
|
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||||
@@ -96,7 +94,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
ExecStart = lib.getExe cfg.package + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -12,7 +11,6 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
dmsPkgs
|
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -36,7 +34,7 @@ in
|
|||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
ExecStart = lib.getExe cfg.package + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell = {
|
options.programs.dank-material-shell = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell";
|
enable = lib.mkEnableOption "DankMaterialShell";
|
||||||
|
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
||||||
|
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
||||||
|
};
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Architecture: any
|
|||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
greetd,
|
greetd,
|
||||||
quickshell-git | quickshell
|
quickshell-git | quickshell
|
||||||
Recommends: niri | hyprland | sway
|
Suggests: niri | hyprland | sway
|
||||||
Description: DankMaterialShell greeter for greetd
|
Description: DankMaterialShell greeter for greetd
|
||||||
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
inspired greeter interface built with Quickshell for Wayland compositors.
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
|||||||
10
flake.lock
generated
10
flake.lock
generated
@@ -23,16 +23,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766725085,
|
"lastModified": 1776854048,
|
||||||
"narHash": "sha256-O2aMFdDUYJazFrlwL7aSIHbUSEm3ADVZjmf41uBJfHs=",
|
"narHash": "sha256-lLbV66V3RMNp1l8/UelmR4YzoJ5ONtgvEtiUMJATH/o=",
|
||||||
"ref": "refs/heads/master",
|
"ref": "refs/heads/master",
|
||||||
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
|
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
|
||||||
"revCount": 715,
|
"revCount": 806,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"rev": "41828c4180fb921df7992a5405f5ff05d2ac2fff",
|
"rev": "783c953987dc56ff0601abe6845ed96f1d00495a",
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
"url": "https://git.outfoxxed.me/quickshell/quickshell"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
quickshell = {
|
quickshell = {
|
||||||
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=41828c4180fb921df7992a5405f5ff05d2ac2fff";
|
url = "git+https://git.outfoxxed.me/quickshell/quickshell?rev=783c953987dc56ff0601abe6845ed96f1d00495a";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -158,10 +158,16 @@ const NIRI_ACTIONS = {
|
|||||||
{ id: "focus-monitor-right", label: "Focus Monitor Right" },
|
{ id: "focus-monitor-right", label: "Focus Monitor Right" },
|
||||||
{ id: "focus-monitor-down", label: "Focus Monitor Down" },
|
{ id: "focus-monitor-down", label: "Focus Monitor Down" },
|
||||||
{ id: "focus-monitor-up", label: "Focus Monitor Up" },
|
{ id: "focus-monitor-up", label: "Focus Monitor Up" },
|
||||||
{ id: "move-column-to-monitor-left", label: "Move to Monitor Left" },
|
{ id: "move-column-to-monitor-left", label: "Move Column to Monitor Left" },
|
||||||
{ id: "move-column-to-monitor-right", label: "Move to Monitor Right" },
|
{ id: "move-column-to-monitor-right", label: "Move Column to Monitor Right" },
|
||||||
{ id: "move-column-to-monitor-down", label: "Move to Monitor Down" },
|
{ id: "move-column-to-monitor-down", label: "Move Column to Monitor Down" },
|
||||||
{ id: "move-column-to-monitor-up", label: "Move to Monitor Up" }
|
{ id: "move-column-to-monitor-up", label: "Move Column to Monitor Up" },
|
||||||
|
{ id: "move-workspace-to-monitor-left", label: "Move Workspace to Monitor Left" },
|
||||||
|
{ id: "move-workspace-to-monitor-right", label: "Move Workspace to Monitor Right" },
|
||||||
|
{ id: "move-workspace-to-monitor-down", label: "Move Workspace to Monitor Down" },
|
||||||
|
{ id: "move-workspace-to-monitor-up", label: "Move Workspace to Monitor Up" },
|
||||||
|
{ id: "move-workspace-to-monitor-next", label: "Move Workspace to Next Monitor" },
|
||||||
|
{ id: "move-workspace-to-monitor-previous", label: "Move Workspace to Previous Monitor" }
|
||||||
],
|
],
|
||||||
"Screenshot": [
|
"Screenshot": [
|
||||||
{ id: "screenshot", label: "Screenshot (Interactive)" },
|
{ id: "screenshot", label: "Screenshot (Interactive)" },
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function expandTilde(path: string): string {
|
function expandTilde(path: string): string {
|
||||||
return strip(path.replace("~", stringify(root.home)));
|
if (!path.startsWith("~"))
|
||||||
|
return path;
|
||||||
|
return strip(root.home) + path.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortenHome(path: string): string {
|
function shortenHome(path: string): string {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -338,8 +338,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;
|
||||||
@@ -1091,15 +1091,16 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncWallpaperForCurrentMode() {
|
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) {
|
||||||
@@ -1204,7 +1205,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -165,6 +165,18 @@ Singleton {
|
|||||||
property int modalCustomAnimationDuration: 150
|
property int modalCustomAnimationDuration: 150
|
||||||
property bool enableRippleEffects: true
|
property bool enableRippleEffects: true
|
||||||
onEnableRippleEffectsChanged: saveSettings()
|
onEnableRippleEffectsChanged: saveSettings()
|
||||||
|
property bool blurEnabled: false
|
||||||
|
onBlurEnabledChanged: saveSettings()
|
||||||
|
property bool blurForegroundLayers: true
|
||||||
|
onBlurForegroundLayersChanged: saveSettings()
|
||||||
|
property real blurLayerOutlineOpacity: 0.12
|
||||||
|
onBlurLayerOutlineOpacityChanged: saveSettings()
|
||||||
|
property string blurBorderColor: "outline"
|
||||||
|
onBlurBorderColorChanged: saveSettings()
|
||||||
|
property string blurBorderCustomColor: "#ffffff"
|
||||||
|
onBlurBorderCustomColorChanged: saveSettings()
|
||||||
|
property real blurBorderOpacity: 0.35
|
||||||
|
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
|
||||||
@@ -182,6 +194,9 @@ Singleton {
|
|||||||
property int selectedGpuIndex: 0
|
property int selectedGpuIndex: 0
|
||||||
property var enabledGpuPciIds: []
|
property var enabledGpuPciIds: []
|
||||||
property bool showSystemTray: true
|
property bool showSystemTray: true
|
||||||
|
property string systemTrayIconTintMode: "none"
|
||||||
|
property int systemTrayIconTintSaturation: 50
|
||||||
|
property int systemTrayIconTintStrength: 135
|
||||||
property bool showClock: true
|
property bool showClock: true
|
||||||
property bool showNotificationButton: true
|
property bool showNotificationButton: true
|
||||||
property bool showBattery: true
|
property bool showBattery: true
|
||||||
@@ -294,6 +309,17 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
|
property bool greeterRememberLastSession: true
|
||||||
|
property bool greeterRememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -495,6 +521,23 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
|
property bool lockFingerprintCanEnable: false
|
||||||
|
property bool lockFingerprintReady: false
|
||||||
|
property string lockFingerprintReason: "probe_failed"
|
||||||
|
property bool greeterFingerprintCanEnable: false
|
||||||
|
property bool greeterFingerprintReady: false
|
||||||
|
property string greeterFingerprintReason: "probe_failed"
|
||||||
|
property string greeterFingerprintSource: "none"
|
||||||
|
property bool enableU2f: false
|
||||||
|
property string u2fMode: "or"
|
||||||
|
property bool u2fAvailable: false
|
||||||
|
property bool lockU2fCanEnable: false
|
||||||
|
property bool lockU2fReady: false
|
||||||
|
property string lockU2fReason: "probe_failed"
|
||||||
|
property bool greeterU2fCanEnable: false
|
||||||
|
property bool greeterU2fReady: false
|
||||||
|
property string greeterU2fReason: "probe_failed"
|
||||||
|
property string greeterU2fSource: "none"
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
@@ -977,12 +1020,19 @@ Singleton {
|
|||||||
signal widgetDataChanged
|
signal widgetDataChanged
|
||||||
signal workspaceIconsUpdated
|
signal workspaceIconsUpdated
|
||||||
|
|
||||||
|
function refreshAuthAvailability() {
|
||||||
|
if (isGreeterMode)
|
||||||
|
return;
|
||||||
|
Processes.settingsRoot = root;
|
||||||
|
Processes.detectAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
Processes.detectFprintd();
|
refreshAuthAvailability();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1249,9 +1299,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() {
|
||||||
|
|||||||
@@ -346,12 +346,11 @@ Singleton {
|
|||||||
function onLoginctlEvent(event) {
|
function onLoginctlEvent(event) {
|
||||||
if (!SessionData.themeModeAutoEnabled)
|
if (!SessionData.themeModeAutoEnabled)
|
||||||
return;
|
return;
|
||||||
if (event.event === "unlock" || event.event === "resume") {
|
if (typeof SettingsData !== "undefined" && SettingsData.loginctlLockIntegration)
|
||||||
if (!themeAutoBackendAvailable()) {
|
return;
|
||||||
root.evaluateThemeMode();
|
const eventType = String(event?.type || event?.event || "").toLowerCase();
|
||||||
return;
|
if (eventType === "unlock") {
|
||||||
}
|
root.triggerThemeAutomationRefresh();
|
||||||
DMSService.sendRequest("theme.auto.trigger", {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +413,27 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionService
|
||||||
|
enabled: typeof SessionService !== "undefined" && typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled
|
||||||
|
|
||||||
|
function onSessionUnlocked() {
|
||||||
|
root.triggerThemeAutomationRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSessionResumed() {
|
||||||
|
root.triggerThemeAutomationRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerThemeAutomationRefresh() {
|
||||||
|
if (!themeAutoBackendAvailable()) {
|
||||||
|
root.evaluateThemeMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DMSService.sendRequest("theme.auto.trigger", {});
|
||||||
|
}
|
||||||
|
|
||||||
function applyGreeterTheme(themeName) {
|
function applyGreeterTheme(themeName) {
|
||||||
switchTheme(themeName, false, false);
|
switchTheme(themeName, false, false);
|
||||||
if (themeName === dynamic && dynamicColorsFileView.path) {
|
if (themeName === dynamic && dynamicColorsFileView.path) {
|
||||||
@@ -541,8 +561,8 @@ Singleton {
|
|||||||
property color success: currentThemeData.success || "#4CAF50"
|
property color success: currentThemeData.success || "#4CAF50"
|
||||||
|
|
||||||
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
property color primaryHover: Qt.rgba(primary.r, primary.g, primary.b, 0.12)
|
||||||
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, 0.08)
|
property color primaryHoverLight: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.12 : 0.08)
|
||||||
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, 0.16)
|
property color primaryPressed: Qt.rgba(primary.r, primary.g, primary.b, transparentBlurLayers ? 0.24 : 0.16)
|
||||||
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
property color primarySelected: Qt.rgba(primary.r, primary.g, primary.b, 0.3)
|
||||||
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
property color primaryBackground: Qt.rgba(primary.r, primary.g, primary.b, 0.04)
|
||||||
|
|
||||||
@@ -551,17 +571,28 @@ Singleton {
|
|||||||
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
property color surfaceHover: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.08)
|
||||||
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
property color surfacePressed: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.12)
|
||||||
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
property color surfaceSelected: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.15)
|
||||||
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.1)
|
property color surfaceLight: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, transparentBlurLayers ? 0.3 : 0.1)
|
||||||
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
property color surfaceVariantAlpha: Qt.rgba(surfaceVariant.r, surfaceVariant.g, surfaceVariant.b, 0.2)
|
||||||
|
|
||||||
|
readonly property bool blurForegroundLayers: BlurService.enabled && (typeof SettingsData === "undefined" || (SettingsData.blurForegroundLayers ?? true))
|
||||||
|
readonly property bool transparentBlurLayers: BlurService.enabled && !blurForegroundLayers
|
||||||
|
readonly property color readableSurface: withAlpha(surfaceContainer, popupTransparency)
|
||||||
|
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
|
||||||
|
readonly property color floatingSurface: transparentBlurLayers ? "transparent" : readableSurface
|
||||||
|
readonly property color floatingSurfaceHigh: transparentBlurLayers ? "transparent" : readableSurfaceHigh
|
||||||
|
readonly property color nestedSurface: floatingSurfaceHigh
|
||||||
|
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
|
||||||
|
readonly property real layerOutlineOpacity: BlurService.enabled ? blurLayerOutlineOpacity : 0.08
|
||||||
|
readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0
|
||||||
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
|
||||||
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
|
||||||
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
|
||||||
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
|
||||||
|
|
||||||
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
property color outlineButton: Qt.rgba(outline.r, outline.g, outline.b, 0.5)
|
||||||
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, 0.05)
|
property color outlineLight: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 0.625) : 0.05)
|
||||||
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, 0.08)
|
property color outlineMedium: Qt.rgba(outline.r, outline.g, outline.b, layerOutlineOpacity)
|
||||||
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, 0.12)
|
property color outlineStrong: Qt.rgba(outline.r, outline.g, outline.b, BlurService.enabled ? Math.min(1, layerOutlineOpacity * 1.5) : 0.12)
|
||||||
|
|
||||||
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
property color errorHover: Qt.rgba(error.r, error.g, error.b, 0.12)
|
||||||
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
property color errorPressed: Qt.rgba(error.r, error.g, error.b, 0.16)
|
||||||
@@ -579,6 +610,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property color ccTileInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.16) : (blurForegroundLayers ? withAlpha(surfaceContainerHigh, Math.min(popupTransparency, 0.24)) : withAlpha(surfaceContainer, popupTransparency))
|
||||||
|
readonly property color ccPillInactiveBg: transparentBlurLayers ? withAlpha(surfaceContainerHigh, 0.08) : nestedSurface
|
||||||
|
readonly property color ccPillInactiveHoverBg: transparentBlurLayers ? withAlpha(primary, 0.10) : primaryPressed
|
||||||
|
readonly property color ccSliderTrackColor: transparentBlurLayers ? surfaceText : surfaceContainerHigh
|
||||||
|
readonly property real ccSliderTrackOpacity: transparentBlurLayers ? 0.18 : popupTransparency
|
||||||
|
|
||||||
readonly property color ccTileActiveText: {
|
readonly property color ccTileActiveText: {
|
||||||
switch (SettingsData.controlCenterTileColorMode) {
|
switch (SettingsData.controlCenterTileColorMode) {
|
||||||
case "primaryContainer":
|
case "primaryContainer":
|
||||||
@@ -858,7 +895,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.fontFamily;
|
return GreetdSettings.getEffectiveFontFamily();
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -1022,7 +1059,11 @@ Singleton {
|
|||||||
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
|
||||||
const defaults = themeData.variants.defaults || {};
|
const defaults = themeData.variants.defaults || {};
|
||||||
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
const modeDefaults = defaults[colorMode] || defaults.dark || {};
|
||||||
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const stored = isGreeterMode ?
|
||||||
|
(GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) :
|
||||||
|
(typeof SettingsData !== "undefined" ?
|
||||||
|
SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
|
||||||
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
var flavorId = stored.flavor || modeDefaults.flavor || "";
|
||||||
const accentId = stored.accent || modeDefaults.accent || "";
|
const accentId = stored.accent || modeDefaults.accent || "";
|
||||||
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
var flavor = findVariant(themeData.variants.flavors, flavorId);
|
||||||
@@ -1048,7 +1089,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
if (themeData.variants.options && themeData.variants.options.length > 0) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode
|
||||||
|
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
|
||||||
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
const variant = findVariant(themeData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
const variantColors = variant[colorMode] || variant.dark || variant.light || {};
|
||||||
@@ -1093,7 +1137,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadCustomThemeFromFile(filePath) {
|
function loadCustomThemeFromFile(filePath) {
|
||||||
customThemeFileView.path = filePath;
|
customThemeFileView.path = Paths.expandTilde(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadCustomThemeVariant() {
|
function reloadCustomThemeVariant() {
|
||||||
@@ -1420,8 +1464,13 @@ Singleton {
|
|||||||
const defaults = customThemeRawData.variants.defaults || {};
|
const defaults = customThemeRawData.variants.defaults || {};
|
||||||
const darkDefaults = defaults.dark || {};
|
const darkDefaults = defaults.dark || {};
|
||||||
const lightDefaults = defaults.light || defaults.dark || {};
|
const lightDefaults = defaults.light || defaults.dark || {};
|
||||||
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
|
const storedDark = isGreeterMode
|
||||||
|
? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
|
||||||
|
const storedLight = isGreeterMode
|
||||||
|
? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
|
||||||
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
|
||||||
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
|
||||||
const accentId = storedDark.accent || darkDefaults.accent || "";
|
const accentId = storedDark.accent || darkDefaults.accent || "";
|
||||||
@@ -1439,7 +1488,10 @@ Singleton {
|
|||||||
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
|
||||||
}
|
}
|
||||||
} else if (customThemeRawData.variants.options) {
|
} else if (customThemeRawData.variants.options) {
|
||||||
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default;
|
const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
|
||||||
|
const selectedVariantId = isGreeterMode
|
||||||
|
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default)
|
||||||
|
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
|
||||||
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
|
||||||
if (variant) {
|
if (variant) {
|
||||||
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
darkTheme = mergeColors(darkTheme, variant.dark || {});
|
||||||
@@ -1734,6 +1786,7 @@ Singleton {
|
|||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: customThemeFileView
|
id: customThemeFileView
|
||||||
|
blockLoading: false
|
||||||
watchChanges: currentTheme === "custom"
|
watchChanges: currentTheme === "custom"
|
||||||
|
|
||||||
function parseAndLoadTheme() {
|
function parseAndLoadTheme() {
|
||||||
@@ -1763,10 +1816,11 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
blockLoading: false
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: !SessionData.isGreeterMode
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
|
|||||||
@@ -10,18 +10,350 @@ Singleton {
|
|||||||
|
|
||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
|
property string greetdPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
property string systemLoginPamText: ""
|
||||||
|
property string systemLocalLoginPamText: ""
|
||||||
|
property string commonAuthPcPamText: ""
|
||||||
|
property string loginPamText: ""
|
||||||
|
property string dankshellU2fPamText: ""
|
||||||
|
property string u2fKeysText: ""
|
||||||
|
|
||||||
|
property string fingerprintProbeOutput: ""
|
||||||
|
property int fingerprintProbeExitCode: 0
|
||||||
|
property bool fingerprintProbeStreamFinished: false
|
||||||
|
property bool fingerprintProbeExited: false
|
||||||
|
property string fingerprintProbeState: "probe_failed"
|
||||||
|
|
||||||
|
property string pamSupportProbeOutput: ""
|
||||||
|
property bool pamSupportProbeStreamFinished: false
|
||||||
|
property bool pamSupportProbeExited: false
|
||||||
|
property int pamSupportProbeExitCode: 0
|
||||||
|
property bool pamFprintSupportDetected: false
|
||||||
|
property bool pamU2fSupportDetected: false
|
||||||
|
|
||||||
|
readonly property string homeDir: Quickshell.env("HOME") || ""
|
||||||
|
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
|
||||||
|
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
|
||||||
|
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
|
||||||
|
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
|
||||||
|
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||||
|
|
||||||
|
function envFlag(name) {
|
||||||
|
const value = (Quickshell.env(name) || "").trim().toLowerCase();
|
||||||
|
if (value === "1" || value === "true" || value === "yes" || value === "on")
|
||||||
|
return true;
|
||||||
|
if (value === "0" || value === "false" || value === "no" || value === "off")
|
||||||
|
return false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
|
||||||
|
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
|
||||||
|
|
||||||
function detectQtTools() {
|
function detectQtTools() {
|
||||||
qtToolsDetectionProcess.running = true;
|
qtToolsDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectAuthCapabilities() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (forcedFprintAvailable === null) {
|
||||||
|
fingerprintProbeOutput = "";
|
||||||
|
fingerprintProbeStreamFinished = false;
|
||||||
|
fingerprintProbeExited = false;
|
||||||
|
fingerprintProbeProcess.running = true;
|
||||||
|
} else {
|
||||||
|
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
pamFprintSupportDetected = false;
|
||||||
|
pamU2fSupportDetected = false;
|
||||||
|
pamSupportProbeOutput = "";
|
||||||
|
pamSupportProbeStreamFinished = false;
|
||||||
|
pamSupportProbeExited = false;
|
||||||
|
pamSupportDetectionProcess.running = true;
|
||||||
|
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
function detectFprintd() {
|
function detectFprintd() {
|
||||||
fprintdDetectionProcess.running = true;
|
detectAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectU2f() {
|
||||||
|
detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamTextIncludesFile(pamText, filename) {
|
||||||
|
if (!pamText || !filename)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function greeterPamStackHasModule(moduleName) {
|
||||||
|
if (pamModuleEnabled(greetdPamText, moduleName))
|
||||||
|
return true;
|
||||||
|
const includedPamStacks = [
|
||||||
|
["system-auth", systemAuthPamText],
|
||||||
|
["common-auth", commonAuthPamText],
|
||||||
|
["password-auth", passwordAuthPamText],
|
||||||
|
["system-login", systemLoginPamText],
|
||||||
|
["system-local-login", systemLocalLoginPamText],
|
||||||
|
["common-auth-pc", commonAuthPcPamText],
|
||||||
|
["login", loginPamText]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < includedPamStacks.length; i++) {
|
||||||
|
const stack = includedPamStacks[i];
|
||||||
|
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEnrolledFingerprintOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
|
||||||
|
return true;
|
||||||
|
const lines = lower.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const trimmed = lines[i].trim();
|
||||||
|
if (trimmed.startsWith("finger:"))
|
||||||
|
return true;
|
||||||
|
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMissingFingerprintEnrollmentOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
return lower.includes("no fingers enrolled")
|
||||||
|
|| lower.includes("no fingerprints enrolled")
|
||||||
|
|| lower.includes("no prints enrolled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMissingFingerprintReaderOutput(output) {
|
||||||
|
const lower = (output || "").toLowerCase();
|
||||||
|
return lower.includes("no devices available")
|
||||||
|
|| lower.includes("no device available")
|
||||||
|
|| lower.includes("no devices found")
|
||||||
|
|| lower.includes("list_devices failed")
|
||||||
|
|| lower.includes("no device");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFingerprintProbe(exitCode, output) {
|
||||||
|
if (hasEnrolledFingerprintOutput(output))
|
||||||
|
return "ready";
|
||||||
|
if (hasMissingFingerprintEnrollmentOutput(output))
|
||||||
|
return "missing_enrollment";
|
||||||
|
if (hasMissingFingerprintReaderOutput(output))
|
||||||
|
return "missing_reader";
|
||||||
|
if (exitCode === 0)
|
||||||
|
return "missing_enrollment";
|
||||||
|
if (exitCode === 127 || (output || "").includes("__missing_command__"))
|
||||||
|
return "probe_failed";
|
||||||
|
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockFingerprintCapability(canEnable, ready, reason) {
|
||||||
|
settingsRoot.lockFingerprintCanEnable = canEnable;
|
||||||
|
settingsRoot.lockFingerprintReady = ready;
|
||||||
|
settingsRoot.lockFingerprintReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLockU2fCapability(canEnable, ready, reason) {
|
||||||
|
settingsRoot.lockU2fCanEnable = canEnable;
|
||||||
|
settingsRoot.lockU2fReady = ready;
|
||||||
|
settingsRoot.lockU2fReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
|
||||||
|
settingsRoot.greeterFingerprintCanEnable = canEnable;
|
||||||
|
settingsRoot.greeterFingerprintReady = ready;
|
||||||
|
settingsRoot.greeterFingerprintReason = reason;
|
||||||
|
settingsRoot.greeterFingerprintSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGreeterU2fCapability(canEnable, ready, reason, source) {
|
||||||
|
settingsRoot.greeterU2fCanEnable = canEnable;
|
||||||
|
settingsRoot.greeterU2fReady = ready;
|
||||||
|
settingsRoot.greeterU2fReason = reason;
|
||||||
|
settingsRoot.greeterU2fSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeFingerprintCapabilities() {
|
||||||
|
if (forcedFprintAvailable !== null) {
|
||||||
|
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
|
||||||
|
const source = forcedFprintAvailable ? "dms" : "none";
|
||||||
|
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
|
||||||
|
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = fingerprintProbeState;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setLockFingerprintCapability(true, true, "ready");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setLockFingerprintCapability(true, false, "missing_enrollment");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setLockFingerprintCapability(false, false, "missing_reader");
|
||||||
|
break;
|
||||||
|
case "missing_pam_support":
|
||||||
|
setLockFingerprintCapability(false, false, "missing_pam_support");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setLockFingerprintCapability(false, false, "probe_failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (greeterPamHasFprint) {
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case "ready":
|
||||||
|
setGreeterFingerprintCapability(true, true, "ready", "dms");
|
||||||
|
break;
|
||||||
|
case "missing_enrollment":
|
||||||
|
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
|
||||||
|
break;
|
||||||
|
case "missing_reader":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
|
||||||
|
break;
|
||||||
|
case "missing_pam_support":
|
||||||
|
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeU2fCapabilities() {
|
||||||
|
if (forcedU2fAvailable !== null) {
|
||||||
|
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
|
||||||
|
const source = forcedU2fAvailable ? "dms" : "none";
|
||||||
|
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
|
||||||
|
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
|
||||||
|
const lockCanEnable = lockReady || pamU2fSupportDetected;
|
||||||
|
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||||
|
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
|
||||||
|
|
||||||
|
if (greeterPamHasU2f) {
|
||||||
|
setGreeterU2fCapability(true, true, "configured_externally", "pam");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const greeterReady = homeU2fKeysDetected;
|
||||||
|
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
|
||||||
|
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
|
||||||
|
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
function recomputeAuthCapabilities() {
|
||||||
|
if (!settingsRoot)
|
||||||
|
return;
|
||||||
|
recomputeFingerprintCapabilities();
|
||||||
|
recomputeU2fCapabilities();
|
||||||
|
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
|
||||||
|
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeFingerprintProbe() {
|
||||||
|
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
|
||||||
|
return;
|
||||||
|
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizePamSupportProbe() {
|
||||||
|
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pamFprintSupportDetected = false;
|
||||||
|
pamU2fSupportDetected = false;
|
||||||
|
|
||||||
|
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const parts = lines[i].split(":");
|
||||||
|
if (parts.length !== 2)
|
||||||
|
continue;
|
||||||
|
if (parts[0] === "pam_fprintd.so")
|
||||||
|
pamFprintSupportDetected = parts[1] === "true";
|
||||||
|
else if (parts[0] === "pam_u2f.so")
|
||||||
|
pamU2fSupportDetected = parts[1] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
|
||||||
|
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
|
||||||
|
|
||||||
|
recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
property var qtToolsDetectionProcess: Process {
|
property var qtToolsDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
|
||||||
running: false
|
running: false
|
||||||
@@ -31,15 +363,15 @@ Singleton {
|
|||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
var lines = text.trim().split('\n');
|
const lines = text.trim().split("\n");
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('qt5ct:')) {
|
if (line.startsWith("qt5ct:")) {
|
||||||
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true';
|
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
|
||||||
} else if (line.startsWith('qt6ct:')) {
|
} else if (line.startsWith("qt6ct:")) {
|
||||||
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true';
|
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
|
||||||
} else if (line.startsWith('gtk:')) {
|
} else if (line.startsWith("gtk:")) {
|
||||||
settingsRoot.gtkAvailable = line.split(':')[1] === 'true';
|
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +379,181 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var fprintdDetectionProcess: Process {
|
property var fingerprintProbeProcess: Process {
|
||||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.fingerprintProbeOutput = text || "";
|
||||||
|
root.fingerprintProbeStreamFinished = true;
|
||||||
|
root.finalizeFingerprintProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
if (!settingsRoot)
|
root.fingerprintProbeExitCode = exitCode;
|
||||||
return;
|
root.fingerprintProbeExited = true;
|
||||||
settingsRoot.fprintdAvailable = (exitCode === 0);
|
root.finalizeFingerprintProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var pamSupportDetectionProcess: Process {
|
||||||
|
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.pamSupportProbeOutput = text || "";
|
||||||
|
root.pamSupportProbeStreamFinished = true;
|
||||||
|
root.finalizePamSupportProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: function (exitCode) {
|
||||||
|
root.pamSupportProbeExitCode = exitCode;
|
||||||
|
root.pamSupportProbeExited = true;
|
||||||
|
root.finalizePamSupportProbe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLoginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLoginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemLocalLoginPamWatcher
|
||||||
|
path: "/etc/pam.d/system-local-login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemLocalLoginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemLocalLoginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPcPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth-pc"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPcPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPcPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: loginPamWatcher
|
||||||
|
path: "/etc/pam.d/login"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.loginPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.loginPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: dankshellU2fPamWatcher
|
||||||
|
path: "/etc/pam.d/dankshell-u2f"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.dankshellU2fPamText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.dankshellU2fPamText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: u2fKeysWatcher
|
||||||
|
path: root.u2fKeysPath
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.u2fKeysText = text();
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.u2fKeysText = "";
|
||||||
|
root.recomputeAuthCapabilities();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ var SPEC = {
|
|||||||
modalAnimationSpeed: { def: 1 },
|
modalAnimationSpeed: { def: 1 },
|
||||||
modalCustomAnimationDuration: { def: 150 },
|
modalCustomAnimationDuration: { def: 150 },
|
||||||
enableRippleEffects: { def: true },
|
enableRippleEffects: { def: true },
|
||||||
|
blurEnabled: { def: false },
|
||||||
|
blurForegroundLayers: { def: true },
|
||||||
|
blurLayerOutlineOpacity: { def: 0.12, coerce: percentToUnit },
|
||||||
|
blurBorderColor: { def: "outline" },
|
||||||
|
blurBorderCustomColor: { def: "#ffffff" },
|
||||||
|
blurBorderOpacity: { def: 0.35, coerce: percentToUnit },
|
||||||
wallpaperFillMode: { def: "Fill" },
|
wallpaperFillMode: { def: "Fill" },
|
||||||
blurredWallpaperLayer: { def: false },
|
blurredWallpaperLayer: { def: false },
|
||||||
blurWallpaperOnOverview: { def: false },
|
blurWallpaperOnOverview: { def: false },
|
||||||
@@ -63,6 +69,9 @@ var SPEC = {
|
|||||||
selectedGpuIndex: { def: 0 },
|
selectedGpuIndex: { def: 0 },
|
||||||
enabledGpuPciIds: { def: [] },
|
enabledGpuPciIds: { def: [] },
|
||||||
showSystemTray: { def: true },
|
showSystemTray: { def: true },
|
||||||
|
systemTrayIconTintMode: { def: "none" },
|
||||||
|
systemTrayIconTintSaturation: { def: 50 },
|
||||||
|
systemTrayIconTintStrength: { def: 135 },
|
||||||
showClock: { def: true },
|
showClock: { def: true },
|
||||||
showNotificationButton: { def: true },
|
showNotificationButton: { def: true },
|
||||||
showBattery: { def: true },
|
showBattery: { def: true },
|
||||||
@@ -154,6 +163,17 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
|
greeterRememberLastSession: { def: true },
|
||||||
|
greeterRememberLastUser: { def: true },
|
||||||
|
greeterEnableFprint: { def: false },
|
||||||
|
greeterEnableU2f: { def: false },
|
||||||
|
greeterWallpaperPath: { def: "" },
|
||||||
|
greeterUse24HourClock: { def: true },
|
||||||
|
greeterShowSeconds: { def: false },
|
||||||
|
greeterPadHours12Hour: { def: false },
|
||||||
|
greeterLockDateFormat: { def: "" },
|
||||||
|
greeterFontFamily: { def: "" },
|
||||||
|
greeterWallpaperFillMode: { def: "" },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
@@ -318,6 +338,23 @@ var SPEC = {
|
|||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
|
lockFingerprintCanEnable: { def: false, persist: false },
|
||||||
|
lockFingerprintReady: { def: false, persist: false },
|
||||||
|
lockFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterFingerprintCanEnable: { def: false, persist: false },
|
||||||
|
greeterFingerprintReady: { def: false, persist: false },
|
||||||
|
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterFingerprintSource: { def: "none", persist: false },
|
||||||
|
enableU2f: { def: false },
|
||||||
|
u2fMode: { def: "or" },
|
||||||
|
u2fAvailable: { def: false, persist: false },
|
||||||
|
lockU2fCanEnable: { def: false, persist: false },
|
||||||
|
lockU2fReady: { def: false, persist: false },
|
||||||
|
lockU2fReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterU2fCanEnable: { def: false, persist: false },
|
||||||
|
greeterU2fReady: { def: false, persist: false },
|
||||||
|
greeterU2fReason: { def: "probe_failed", persist: false },
|
||||||
|
greeterU2fSource: { def: "none", persist: false },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Greetd
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.Greetd
|
import qs.Modules.Greetd
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool osdSurfacesLoaded: true
|
||||||
|
property int pendingOsdResumeReloads: 0
|
||||||
|
|
||||||
|
function recreateOsdSurfaces() {
|
||||||
|
OSDManager.currentOSDsByScreen = ({});
|
||||||
|
osdSurfacesLoaded = false;
|
||||||
|
osdSurfaceReloadTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
Instantiator {
|
Instantiator {
|
||||||
id: daemonPluginInstantiator
|
id: daemonPluginInstantiator
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
@@ -221,6 +230,33 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: osdResumeRecreateTimer
|
||||||
|
interval: 400
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
root.recreateOsdSurfaces();
|
||||||
|
|
||||||
|
if (root.pendingOsdResumeReloads > 1) {
|
||||||
|
root.pendingOsdResumeReloads--;
|
||||||
|
interval = 1400;
|
||||||
|
restart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.pendingOsdResumeReloads = 0;
|
||||||
|
interval = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: osdSurfaceReloadTimer
|
||||||
|
interval: 120
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.osdSurfacesLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
@@ -716,6 +752,16 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionService
|
||||||
|
|
||||||
|
function onSessionResumed() {
|
||||||
|
root.pendingOsdResumeReloads = 2;
|
||||||
|
osdResumeRecreateTimer.interval = 400;
|
||||||
|
osdResumeRecreateTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankColorPickerModal {
|
DankColorPickerModal {
|
||||||
id: colorPickerModal
|
id: colorPickerModal
|
||||||
|
|
||||||
@@ -798,9 +844,8 @@ Item {
|
|||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Notepad {
|
Notepad {
|
||||||
onHideRequested: {
|
slideout: notepadSlideout
|
||||||
notepadSlideout.hide();
|
onHideRequested: notepadSlideout.hide()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,51 +936,85 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
Loader {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
id: osdSurfacesLoader
|
||||||
|
active: root.osdSurfacesLoaded
|
||||||
|
asynchronous: false
|
||||||
|
|
||||||
delegate: VolumeOSD {
|
sourceComponent: Component {
|
||||||
modelData: item
|
Item {
|
||||||
}
|
Variants {
|
||||||
}
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
Variants {
|
delegate: VolumeOSD {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: MediaVolumeOSD {
|
Variants {
|
||||||
modelData: item
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
delegate: MediaVolumeOSD {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: MediaPlaybackOSD {
|
Variants {
|
||||||
modelData: item
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
delegate: MediaPlaybackOSD {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: MicMuteOSD {
|
Variants {
|
||||||
modelData: item
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
delegate: MicMuteOSD {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: BrightnessOSD {
|
Variants {
|
||||||
modelData: item
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
delegate: BrightnessOSD {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: IdleInhibitorOSD {
|
Variants {
|
||||||
modelData: item
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: IdleInhibitorOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
|
||||||
|
|
||||||
|
delegate: PowerProfileOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: CapsLockOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
|
delegate: AudioOutputOSD {
|
||||||
|
modelData: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,30 +1024,6 @@ Item {
|
|||||||
source: "Services/PowerProfileWatcher.qml"
|
source: "Services/PowerProfileWatcher.qml"
|
||||||
}
|
}
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.osdPowerProfileEnabled ? SettingsData.getFilteredScreens("osd") : []
|
|
||||||
|
|
||||||
delegate: PowerProfileOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: CapsLockOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
|
||||||
|
|
||||||
delegate: AudioOutputOSD {
|
|
||||||
modelData: item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: hyprlandOverviewLoader
|
id: hyprlandOverviewLoader
|
||||||
active: CompositorService.isHyprland
|
active: CompositorService.isHyprland
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No recent clipboard entries found")
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
@@ -195,7 +195,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("No saved clipboard entries")
|
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -60,15 +60,12 @@ DankModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!clipboardAvailable) {
|
|
||||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open();
|
open();
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
shouldHaveFocus = true;
|
shouldHaveFocus = true;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
ClipboardService.refresh();
|
if (clipboardAvailable)
|
||||||
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
|
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
|
|||||||
@@ -50,14 +50,11 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
if (!clipboardAvailable) {
|
|
||||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
open();
|
open();
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
ClipboardService.refresh();
|
if (clipboardAvailable)
|
||||||
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
|
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
@@ -122,10 +119,10 @@ DankPopout {
|
|||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
|
|
||||||
onShouldBeVisibleChanged: {
|
onShouldBeVisibleChanged: {
|
||||||
if (!shouldBeVisible) {
|
if (!shouldBeVisible)
|
||||||
return;
|
return;
|
||||||
}
|
if (clipboardAvailable)
|
||||||
ClipboardService.refresh();
|
ClipboardService.refresh();
|
||||||
keyboardController.reset();
|
keyboardController.reset();
|
||||||
Qt.callLater(function () {
|
Qt.callLater(function () {
|
||||||
if (contentLoader.item?.searchField) {
|
if (contentLoader.item?.searchField) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -211,6 +226,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)
|
||||||
@@ -385,6 +410,15 @@ Item {
|
|||||||
radius: root.cornerRadius
|
radius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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
|
||||||
@@ -124,40 +125,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() {
|
||||||
@@ -181,14 +189,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)
|
||||||
@@ -284,6 +298,16 @@ Item {
|
|||||||
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")) {
|
||||||
@@ -413,6 +437,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ FocusScope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !editMode
|
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: footerBar
|
id: footerBar
|
||||||
@@ -551,7 +551,6 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
|
height: parent.height - searchField.height - categoryRow.height - actionPanel.height - Theme.spacingXS * (categoryRow.visible ? 3 : 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ Popup {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
||||||
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.floatingSurface
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
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: 1
|
border.width: 1
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ Item {
|
|||||||
item: items[i],
|
item: items[i],
|
||||||
flatIndex: flatIdx,
|
flatIndex: flatIdx,
|
||||||
sectionId: sectionId,
|
sectionId: sectionId,
|
||||||
height: 52
|
height: 56
|
||||||
});
|
});
|
||||||
cumY += 52;
|
cumY += 56;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
|
var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
|
||||||
@@ -190,124 +190,135 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankListView {
|
Item {
|
||||||
id: mainListView
|
id: listClip
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: BlurService.enabled && stickyHeader.visible ? 32 : 0
|
||||||
clip: true
|
clip: true
|
||||||
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
|
|
||||||
|
|
||||||
model: ScriptModel {
|
DankListView {
|
||||||
values: root._visualRows
|
id: mainListView
|
||||||
objectProp: "_rowId"
|
y: -listClip.anchors.topMargin
|
||||||
}
|
width: parent.width
|
||||||
|
height: parent.height + listClip.anchors.topMargin
|
||||||
|
clip: true
|
||||||
|
scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
|
||||||
|
|
||||||
add: null
|
model: ScriptModel {
|
||||||
remove: null
|
values: root._visualRows
|
||||||
displaced: null
|
objectProp: "_rowId"
|
||||||
move: null
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: delegateRoot
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: mainListView.width
|
|
||||||
height: modelData?.height ?? 52
|
|
||||||
|
|
||||||
SectionHeader {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: delegateRoot.modelData?.type === "header"
|
|
||||||
section: delegateRoot.modelData?.section ?? null
|
|
||||||
controller: root.controller
|
|
||||||
viewMode: {
|
|
||||||
var vt = root.controller?.viewModeVersion ?? 0;
|
|
||||||
void (vt);
|
|
||||||
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
|
|
||||||
}
|
|
||||||
canChangeViewMode: {
|
|
||||||
var vt = root.controller?.viewModeVersion ?? 0;
|
|
||||||
void (vt);
|
|
||||||
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
|
|
||||||
}
|
|
||||||
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultItem {
|
add: null
|
||||||
anchors.fill: parent
|
remove: null
|
||||||
visible: delegateRoot.modelData?.type === "list_item"
|
displaced: null
|
||||||
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
|
move: null
|
||||||
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
|
||||||
controller: root.controller
|
|
||||||
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
|
|
||||||
|
|
||||||
onClicked: {
|
delegate: Item {
|
||||||
if (root.controller && delegateRoot.modelData?.item) {
|
id: delegateRoot
|
||||||
root.controller.executeItem(delegateRoot.modelData.item);
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: mainListView.width
|
||||||
|
height: modelData?.height ?? 52
|
||||||
|
|
||||||
|
SectionHeader {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: delegateRoot.modelData?.type === "header"
|
||||||
|
section: delegateRoot.modelData?.section ?? null
|
||||||
|
controller: root.controller
|
||||||
|
viewMode: {
|
||||||
|
var vt = root.controller?.viewModeVersion ?? 0;
|
||||||
|
void (vt);
|
||||||
|
return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
|
||||||
|
}
|
||||||
|
canChangeViewMode: {
|
||||||
|
var vt = root.controller?.viewModeVersion ?? 0;
|
||||||
|
void (vt);
|
||||||
|
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
|
||||||
|
}
|
||||||
|
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultItem {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 2
|
||||||
|
anchors.bottomMargin: 2
|
||||||
|
visible: delegateRoot.modelData?.type === "list_item"
|
||||||
|
item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
|
||||||
|
isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller && delegateRoot.modelData?.item) {
|
||||||
|
root.controller.executeItem(delegateRoot.modelData.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRightClicked: (mouseX, mouseY) => {
|
Row {
|
||||||
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
id: gridRowContent
|
||||||
}
|
anchors.fill: parent
|
||||||
}
|
visible: delegateRoot.modelData?.type === "grid_row"
|
||||||
|
|
||||||
Row {
|
Repeater {
|
||||||
id: gridRowContent
|
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
|
||||||
anchors.fill: parent
|
|
||||||
visible: delegateRoot.modelData?.type === "grid_row"
|
|
||||||
|
|
||||||
Repeater {
|
Item {
|
||||||
model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
|
id: gridCellDelegate
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
Item {
|
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
|
||||||
id: gridCellDelegate
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
|
width: cellWidth
|
||||||
|
height: delegateRoot.height
|
||||||
|
|
||||||
width: cellWidth
|
GridItem {
|
||||||
height: delegateRoot.height
|
width: parent.width - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: delegateRoot.modelData?.viewMode === "grid"
|
||||||
|
item: gridCellDelegate.modelData?.item ?? null
|
||||||
|
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||||
|
|
||||||
GridItem {
|
onClicked: {
|
||||||
width: parent.width - 4
|
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||||
height: parent.height - 4
|
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||||
anchors.centerIn: parent
|
}
|
||||||
visible: delegateRoot.modelData?.viewMode === "grid"
|
}
|
||||||
item: gridCellDelegate.modelData?.item ?? null
|
|
||||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
|
||||||
controller: root.controller
|
|
||||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
|
||||||
|
|
||||||
onClicked: {
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRightClicked: (mouseX, mouseY) => {
|
TileItem {
|
||||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
width: parent.width - 4
|
||||||
}
|
height: parent.height - 4
|
||||||
}
|
anchors.centerIn: parent
|
||||||
|
visible: delegateRoot.modelData?.viewMode === "tile"
|
||||||
|
item: gridCellDelegate.modelData?.item ?? null
|
||||||
|
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
|
controller: root.controller
|
||||||
|
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
||||||
|
|
||||||
TileItem {
|
onClicked: {
|
||||||
width: parent.width - 4
|
if (root.controller && gridCellDelegate.modelData?.item) {
|
||||||
height: parent.height - 4
|
root.controller.executeItem(gridCellDelegate.modelData.item);
|
||||||
anchors.centerIn: parent
|
}
|
||||||
visible: delegateRoot.modelData?.viewMode === "tile"
|
|
||||||
item: gridCellDelegate.modelData?.item ?? null
|
|
||||||
isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
|
||||||
controller: root.controller
|
|
||||||
flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (root.controller && gridCellDelegate.modelData?.item) {
|
|
||||||
root.controller.executeItem(gridCellDelegate.modelData.item);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onRightClicked: (mouseX, mouseY) => {
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,6 +335,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;
|
||||||
@@ -363,7 +376,7 @@ Item {
|
|||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
height: 32
|
height: 32
|
||||||
z: 101
|
z: 101
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.floatingSurface
|
||||||
visible: stickyHeaderSection !== null
|
visible: stickyHeaderSection !== null
|
||||||
|
|
||||||
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
||||||
@@ -441,7 +454,7 @@ Item {
|
|||||||
case "apps":
|
case "apps":
|
||||||
return "apps";
|
return "apps";
|
||||||
default:
|
default:
|
||||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
return "search_off";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,9 +482,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Item {
|
|||||||
id: listComponent
|
id: listComponent
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
spacing: 2
|
spacing: 4
|
||||||
width: contentLoader.width
|
width: contentLoader.width
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
|||||||
@@ -225,7 +225,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success")
|
text: {
|
||||||
|
if (root.errorCount === 0)
|
||||||
|
return I18n.tr("All checks passed", "greeter doctor page success");
|
||||||
|
return root.errorCount === 1
|
||||||
|
? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
|
||||||
|
: I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
|
||||||
|
}
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
|||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property var currentFlow: PolkitService.agent?.flow
|
property var currentFlow: PolkitService.agent?.flow
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool awaitingFprintForPassword: false
|
||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
|
|
||||||
|
property string polkitEtcPamText: ""
|
||||||
|
property string polkitLibPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
readonly property bool polkitPamHasFprint: {
|
||||||
|
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||||
|
if (!polkitText)
|
||||||
|
return false;
|
||||||
|
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (line && line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function focusPasswordField() {
|
function focusPasswordField() {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
|||||||
function show() {
|
function show() {
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(focusPasswordField);
|
Qt.callLater(focusPasswordField);
|
||||||
}
|
}
|
||||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
|||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commitSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
|
currentFlow.submit(passwordInput);
|
||||||
|
passwordInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
function submitAuth() {
|
function submitAuth() {
|
||||||
if (!currentFlow || isLoading)
|
if (!currentFlow || isLoading)
|
||||||
return;
|
return;
|
||||||
isLoading = true;
|
if (!currentFlow.isResponseRequired) {
|
||||||
currentFlow.submit(passwordInput);
|
awaitingFprintForPassword = true;
|
||||||
passwordInput = "";
|
return;
|
||||||
|
}
|
||||||
|
_commitSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAuth() {
|
function cancelAuth() {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
currentFlow.cancelAuthenticationRequest();
|
currentFlow.cancelAuthenticationRequest();
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
|||||||
function onIsResponseRequiredChanged() {
|
function onIsResponseRequiredChanged() {
|
||||||
if (!currentFlow.isResponseRequired)
|
if (!currentFlow.isResponseRequired)
|
||||||
return;
|
return;
|
||||||
|
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||||
|
_commitSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
@@ -101,6 +156,41 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitEtcPamText = text()
|
||||||
|
onLoadFailed: root.polkitEtcPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/usr/lib/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitLibPamText = text()
|
||||||
|
onLoadFailed: root.polkitLibPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.systemAuthPamText = text()
|
||||||
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.commonAuthPamText = text()
|
||||||
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
|
|
||||||
@@ -205,36 +295,30 @@ FloatingWindow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: inputFieldHeight
|
height: inputFieldHeight
|
||||||
radius: Theme.cornerRadius
|
backgroundColor: Theme.surfaceHover
|
||||||
color: Theme.surfaceHover
|
normalBorderColor: Theme.outlineStrong
|
||||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
focusedBorderColor: Theme.primary
|
||||||
border.width: passwordField.activeFocus ? 2 : 1
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
|
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||||
|
leftIconSize: 20
|
||||||
|
leftIconColor: Theme.primary
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
opacity: isLoading ? 0.5 : 1
|
opacity: isLoading ? 0.5 : 1
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
MouseArea {
|
textColor: Theme.surfaceText
|
||||||
anchors.fill: parent
|
text: passwordInput
|
||||||
enabled: !isLoading
|
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||||
onClicked: passwordField.forceActiveFocus()
|
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||||
}
|
placeholderText: ""
|
||||||
|
enabled: !isLoading
|
||||||
DankTextField {
|
onTextEdited: passwordInput = text
|
||||||
id: passwordField
|
onAccepted: submitAuth()
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: passwordInput
|
|
||||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
|
||||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: !isLoading
|
|
||||||
onTextEdited: passwordInput = text
|
|
||||||
onAccepted: submitAuth()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -136,7 +136,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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,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)
|
||||||
@@ -218,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)
|
||||||
@@ -300,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.desktopClockEnabled
|
enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.desktopClockEnabled
|
||||||
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.desktopClockTransparency
|
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.desktopClockTransparency
|
||||||
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.desktopClockColorMode
|
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.desktopClockColorMode
|
||||||
property color customColor: isInstance ? (cfg.customColor ?? "#ffffff") : SettingsData.desktopClockCustomColor
|
property color customColor: isInstance ? (cfg.customColor ?? "#ffffff") : SettingsData.desktopClockCustomColor
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Item {
|
|||||||
readonly property var cfg: instanceData?.config ?? null
|
readonly property var cfg: instanceData?.config ?? null
|
||||||
readonly property bool isInstance: instanceId !== "" && cfg !== null
|
readonly property bool isInstance: instanceId !== "" && cfg !== null
|
||||||
|
|
||||||
property bool enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.systemMonitorEnabled
|
enabled: isInstance ? (instanceData?.enabled ?? true) : SettingsData.systemMonitorEnabled
|
||||||
property bool showHeader: isInstance ? (cfg.showHeader ?? true) : SettingsData.systemMonitorShowHeader
|
property bool showHeader: isInstance ? (cfg.showHeader ?? true) : SettingsData.systemMonitorShowHeader
|
||||||
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.systemMonitorTransparency
|
property real transparency: isInstance ? (cfg.transparency ?? 0.8) : SettingsData.systemMonitorTransparency
|
||||||
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.systemMonitorColorMode
|
property string colorMode: isInstance ? (cfg.colorMode ?? "primary") : SettingsData.systemMonitorColorMode
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ 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.nestedSurface
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -252,7 +254,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
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ Rectangle {
|
|||||||
property string text: ""
|
property string text: ""
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property int widgetIndex: 0
|
property int widgetIndex: 0
|
||||||
property var widgetData: null
|
property var widgetData: null
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
@@ -28,12 +27,12 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||||
readonly property color _tileRingActive: Theme.ccTileRing
|
readonly property color _tileRingActive: Theme.ccTileRing
|
||||||
|
|
||||||
color: isActive ? _tileBgActive : _tileBgInactive
|
color: isActive ? _tileBgActive : _tileBgInactive
|
||||||
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||||
border.width: isActive ? 1 : 1
|
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
function hoverTint(base) {
|
function hoverTint(base) {
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
{
|
{
|
||||||
if (!AudioService.sink)
|
if (!AudioService.sink?.audio)
|
||||||
return "volume_off";
|
return "volume_off";
|
||||||
let volume = AudioService.sink.audio.volume;
|
let volume = AudioService.sink.audio.volume;
|
||||||
let muted = AudioService.sink.audio.muted;
|
let muted = AudioService.sink.audio.muted;
|
||||||
@@ -276,7 +276,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
{
|
{
|
||||||
if (!AudioService.source)
|
if (!AudioService.source?.audio)
|
||||||
return "mic_off";
|
return "mic_off";
|
||||||
let muted = AudioService.source.audio.muted;
|
let muted = AudioService.source.audio.muted;
|
||||||
return muted ? "mic_off" : "mic";
|
return muted ? "mic_off" : "mic";
|
||||||
@@ -369,7 +369,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
{
|
{
|
||||||
if (!AudioService.sink)
|
if (!AudioService.sink?.audio)
|
||||||
return I18n.tr("Select device", "audio status");
|
return I18n.tr("Select device", "audio status");
|
||||||
if (AudioService.sink.audio.muted)
|
if (AudioService.sink.audio.muted)
|
||||||
return I18n.tr("Muted", "audio status");
|
return I18n.tr("Muted", "audio status");
|
||||||
@@ -380,7 +380,7 @@ Column {
|
|||||||
}
|
}
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
{
|
{
|
||||||
if (!AudioService.source)
|
if (!AudioService.source?.audio)
|
||||||
return I18n.tr("Select device", "audio status");
|
return I18n.tr("Select device", "audio status");
|
||||||
if (AudioService.source.audio.muted)
|
if (AudioService.source.audio.muted)
|
||||||
return I18n.tr("Muted", "audio status");
|
return I18n.tr("Muted", "audio status");
|
||||||
@@ -412,9 +412,9 @@ Column {
|
|||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
|
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
|
||||||
case "audioOutput":
|
case "audioOutput":
|
||||||
return !!(AudioService.sink && !AudioService.sink.audio.muted);
|
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
|
||||||
case "audioInput":
|
case "audioInput":
|
||||||
return !!(AudioService.source && !AudioService.source.audio.muted);
|
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -507,7 +507,8 @@ Column {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 14
|
height: 14
|
||||||
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
sliderTrackColor: Theme.ccSliderTrackColor
|
||||||
|
sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -529,7 +530,8 @@ Column {
|
|||||||
instanceId: widgetData.instanceId || ""
|
instanceId: widgetData.instanceId || ""
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
parentScreen: root.parentScreen
|
parentScreen: root.parentScreen
|
||||||
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
sliderTrackColor: Theme.ccSliderTrackColor
|
||||||
|
sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
onIconClicked: {
|
onIconClicked: {
|
||||||
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
|
if (!root.editMode && DisplayService.devices && DisplayService.devices.length > 1) {
|
||||||
@@ -552,7 +554,8 @@ Column {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 14
|
height: 14
|
||||||
property color sliderTrackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
sliderTrackColor: Theme.ccSliderTrackColor
|
||||||
|
sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
@@ -10,7 +11,11 @@ Row {
|
|||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
property var availableWidgets: []
|
property var availableWidgets: []
|
||||||
property Item popoutContent: null
|
property var popupScreen: null
|
||||||
|
property real popoutX: 0
|
||||||
|
property real popoutY: 0
|
||||||
|
property real popoutWidth: 0
|
||||||
|
property real popoutHeight: 0
|
||||||
|
|
||||||
signal addWidget(string widgetId)
|
signal addWidget(string widgetId)
|
||||||
signal resetToDefault
|
signal resetToDefault
|
||||||
@@ -19,121 +24,190 @@ Row {
|
|||||||
height: 48
|
height: 48
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
onAddWidget: addWidgetPopup.close()
|
function openWidgetLibrary() {
|
||||||
|
if (popupScreen)
|
||||||
|
addWidgetWindow.screen = popupScreen;
|
||||||
|
addWidgetWindow.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
Popup {
|
function closeWidgetLibrary() {
|
||||||
id: addWidgetPopup
|
addWidgetWindow.visible = false;
|
||||||
parent: popoutContent
|
}
|
||||||
x: parent ? Math.round((parent.width - width) / 2) : 0
|
|
||||||
y: parent ? Math.round((parent.height - height) / 2) : 0
|
|
||||||
width: 400
|
|
||||||
height: 300
|
|
||||||
modal: false
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
background: Rectangle {
|
onAddWidget: closeWidgetLibrary()
|
||||||
color: Theme.surfaceContainer
|
onVisibleChanged: {
|
||||||
border.color: Theme.primarySelected
|
if (!visible)
|
||||||
border.width: 0
|
closeWidgetLibrary();
|
||||||
radius: Theme.cornerRadius
|
}
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: addWidgetWindow
|
||||||
|
|
||||||
|
screen: root.popupScreen
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:control-center-widget-library"
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Item {
|
readonly property bool blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
|
||||||
|
readonly property real surfaceAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.24 : 0.72) : Theme.popupTransparency
|
||||||
|
readonly property real rowAlpha: blurActive ? Math.min(Theme.popupTransparency, Theme.transparentBlurLayers ? 0.10 : 0.52) : Theme.popupTransparency
|
||||||
|
readonly property int panelWidth: 400
|
||||||
|
readonly property int panelHeight: 300
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: addWidgetWindow
|
||||||
|
blurX: widgetLibraryPanel.x
|
||||||
|
blurY: widgetLibraryPanel.y
|
||||||
|
blurWidth: addWidgetWindow.visible ? widgetLibraryPanel.width : 0
|
||||||
|
blurHeight: addWidgetWindow.visible ? widgetLibraryPanel.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingL
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onClicked: root.closeWidgetLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
Row {
|
FocusScope {
|
||||||
id: headerRow
|
anchors.fill: parent
|
||||||
anchors.top: parent.top
|
focus: addWidgetWindow.visible
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
Keys.onEscapePressed: event => {
|
||||||
name: "add_circle"
|
root.closeWidgetLibrary();
|
||||||
size: Theme.iconSize
|
event.accepted = true;
|
||||||
color: Theme.primary
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Typography {
|
Rectangle {
|
||||||
text: I18n.tr("Add Widget")
|
id: widgetLibraryPanel
|
||||||
style: Typography.Style.Subtitle
|
|
||||||
color: Theme.surfaceText
|
width: addWidgetWindow.panelWidth
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
height: addWidgetWindow.panelHeight
|
||||||
}
|
x: Math.round((root.popoutWidth > 0 ? root.popoutX + (root.popoutWidth - width) / 2 : (addWidgetWindow.width - width) / 2))
|
||||||
|
y: Math.round((root.popoutHeight > 0 ? root.popoutY + (root.popoutHeight - height) / 2 : (addWidgetWindow.height - height) / 2))
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, addWidgetWindow.surfaceAlpha)
|
||||||
|
border.color: addWidgetWindow.blurActive ? Theme.outlineMedium : Theme.primarySelected
|
||||||
|
border.width: addWidgetWindow.blurActive ? Theme.layerOutlineWidth : 0
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onClicked: mouse => mouse.accepted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
DankListView {
|
Item {
|
||||||
anchors.top: headerRow.bottom
|
anchors.fill: parent
|
||||||
anchors.topMargin: Theme.spacingM
|
anchors.margins: Theme.spacingL
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
model: root.availableWidgets
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
Row {
|
||||||
width: 400 - Theme.spacingL * 2
|
id: headerRow
|
||||||
height: 50
|
anchors.top: parent.top
|
||||||
radius: Theme.cornerRadius
|
anchors.left: parent.left
|
||||||
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
anchors.right: parent.right
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
spacing: Theme.spacingM
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Row {
|
DankIcon {
|
||||||
anchors.fill: parent
|
name: "add_circle"
|
||||||
anchors.margins: Theme.spacingM
|
size: Theme.iconSize
|
||||||
spacing: Theme.spacingM
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
DankIcon {
|
|
||||||
name: modelData.icon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize
|
|
||||||
|
|
||||||
Typography {
|
|
||||||
text: modelData.text
|
|
||||||
style: Typography.Style.Body
|
|
||||||
color: Theme.surfaceText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
Typography {
|
|
||||||
text: modelData.description
|
|
||||||
style: Typography.Style.Caption
|
|
||||||
color: Theme.outline
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "add"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Typography {
|
||||||
id: widgetMouseArea
|
text: I18n.tr("Add Widget")
|
||||||
anchors.fill: parent
|
style: Typography.Style.Subtitle
|
||||||
hoverEnabled: true
|
color: Theme.surfaceText
|
||||||
cursorShape: Qt.PointingHandCursor
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: {
|
}
|
||||||
root.addWidget(modelData.id);
|
}
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: widgetList
|
||||||
|
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
model: root.availableWidgets
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: widgetList.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: widgetMouseArea.containsMouse ? Theme.withAlpha(Theme.primary, addWidgetWindow.blurActive ? 0.12 : 0.08) : Theme.withAlpha(Theme.surfaceContainerHigh, addWidgetWindow.rowAlpha)
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: Theme.layerOutlineWidth
|
||||||
|
antialiasing: true
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: modelData.icon
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 3
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: modelData.text
|
||||||
|
style: Typography.Style.Body
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
Typography {
|
||||||
|
text: modelData.description
|
||||||
|
style: Typography.Style.Caption
|
||||||
|
color: Theme.outline
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "add"
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: widgetMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.addWidget(modelData.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +245,7 @@ Row {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: addWidgetPopup.open()
|
onClicked: root.openWidgetLibrary()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: 70
|
implicitHeight: 70
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _containerBg: Theme.nestedSurface
|
||||||
|
|
||||||
function openWithSection(section) {
|
function openWithSection(section) {
|
||||||
StateUtils.openWithSection(root, section);
|
StateUtils.openWithSection(root, section);
|
||||||
@@ -210,7 +210,11 @@ DankPopout {
|
|||||||
EditControls {
|
EditControls {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: editMode
|
visible: editMode
|
||||||
popoutContent: controlContent
|
popupScreen: root.screen
|
||||||
|
popoutX: root.alignedX
|
||||||
|
popoutY: root.alignedY
|
||||||
|
popoutWidth: root.alignedWidth
|
||||||
|
popoutHeight: root.alignedHeight
|
||||||
availableWidgets: {
|
availableWidgets: {
|
||||||
if (!editMode)
|
if (!editMode)
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
|
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
@@ -123,6 +123,8 @@ Rectangle {
|
|||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceVariant
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
trackColor: Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (AudioService.source && AudioService.source.audio) {
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
@@ -207,9 +209,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
|
||||||
@@ -351,8 +353,8 @@ Rectangle {
|
|||||||
deviceRipple.trigger(mapped.x, mapped.y);
|
deviceRipple.trigger(mapped.x, mapped.y);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData && modelData.name) {
|
||||||
Pipewire.preferredDefaultAudioSource = modelData;
|
AudioService.setDefaultSourceByName(modelData.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: headerRow
|
id: headerRow
|
||||||
@@ -132,6 +132,8 @@ Rectangle {
|
|||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceVariant
|
thumbOutlineColor: Theme.surfaceVariant
|
||||||
|
trackColor: Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (AudioService.sink && AudioService.sink.audio) {
|
if (AudioService.sink && AudioService.sink.audio) {
|
||||||
@@ -218,9 +220,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
|
||||||
@@ -355,8 +357,8 @@ Rectangle {
|
|||||||
deviceRipple.trigger(mapped.x, mapped.y);
|
deviceRipple.trigger(mapped.x, mapped.y);
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData) {
|
if (modelData && modelData.name) {
|
||||||
Pipewire.preferredDefaultAudioSink = modelData;
|
AudioService.setDefaultSinkByName(modelData.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,9 +399,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
|
||||||
@@ -448,6 +450,7 @@ Rectangle {
|
|||||||
Item {
|
Item {
|
||||||
id: appVolumeRow
|
id: appVolumeRow
|
||||||
property color sliderTrackColor: "transparent"
|
property color sliderTrackColor: "transparent"
|
||||||
|
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|
||||||
height: 40
|
height: 40
|
||||||
@@ -519,7 +522,8 @@ Rectangle {
|
|||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: appVolumeRow.sliderTrackColor.a > 0 ? appVolumeRow.sliderTrackColor : Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: appVolumeRow.sliderTrackOpacity
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (modelData) {
|
if (modelData) {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
function isActiveProfile(profile) {
|
function isActiveProfile(profile) {
|
||||||
if (typeof PowerProfiles === "undefined") {
|
if (typeof PowerProfiles === "undefined") {
|
||||||
@@ -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,9 +153,9 @@ 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.floatingSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
opacity: modalVisible ? 1 : 0
|
opacity: modalVisible ? 1 : 0
|
||||||
scale: modalVisible ? 1 : 0.9
|
scale: modalVisible ? 1 : 0.9
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ Rectangle {
|
|||||||
return headerRow.height + bluetoothContent.height + Theme.spacingM;
|
return headerRow.height + bluetoothContent.height + Theme.spacingM;
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
property var bluetoothCodecModalRef: null
|
property var bluetoothCodecModalRef: null
|
||||||
property var devicesBeingPaired: new Set()
|
property var devicesBeingPaired: new Set()
|
||||||
@@ -115,7 +115,7 @@ Rectangle {
|
|||||||
height: 36
|
height: 36
|
||||||
radius: 18
|
radius: 18
|
||||||
color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
|
color: scanMouseArea.containsMouse && adapterEnabled ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
|
||||||
border.color: adapterEnabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: adapterEnabled ? Theme.primary : Theme.outlineStrong
|
||||||
border.width: 0
|
border.width: 0
|
||||||
visible: adapterEnabled
|
visible: adapterEnabled
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -434,7 +434,7 @@ Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 1
|
height: 1
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
color: Theme.outlineStrong
|
||||||
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -609,7 +609,7 @@ Rectangle {
|
|||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
border.width: 0
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Theme.outlineStrong
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ Rectangle {
|
|||||||
return brightnessContent.height + Theme.spacingM;
|
return brightnessContent.height + Theme.spacingM;
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
id: brightnessContent
|
id: brightnessContent
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ Rectangle {
|
|||||||
|
|
||||||
implicitHeight: diskContent.height + Theme.spacingM
|
implicitHeight: diskContent.height + Theme.spacingM
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
DgopService.addRef(["diskmounts"]);
|
DgopService.addRef(["diskmounts"]);
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ Rectangle {
|
|||||||
return headerRow.height + wifiOffContent.height + Theme.spacingM;
|
return headerRow.height + wifiOffContent.height + Theme.spacingM;
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
NetworkService.addRef();
|
NetworkService.addRef();
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Row {
|
|||||||
|
|
||||||
property var defaultSink: AudioService.sink
|
property var defaultSink: AudioService.sink
|
||||||
property color sliderTrackColor: "transparent"
|
property color sliderTrackColor: "transparent"
|
||||||
|
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
height: 40
|
height: 40
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -35,7 +36,7 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSink) {
|
if (defaultSink?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSink.audio.muted = !defaultSink.audio.muted;
|
defaultSink.audio.muted = !defaultSink.audio.muted;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSink)
|
if (!defaultSink?.audio)
|
||||||
return "volume_off";
|
return "volume_off";
|
||||||
|
|
||||||
let volume = defaultSink.audio.volume;
|
let volume = defaultSink.audio.volume;
|
||||||
@@ -62,28 +63,29 @@ Row {
|
|||||||
return "volume_up";
|
return "volume_up";
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSink?.audio && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
id: volumeSlider
|
id: volumeSlider
|
||||||
|
|
||||||
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSink?.audio ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
enabled: defaultSink !== null
|
enabled: defaultSink?.audio != null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: AudioService.sinkMaxVolume
|
maximum: AudioService.sinkMaxVolume
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: root.sliderTrackOpacity
|
||||||
|
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (defaultSink) {
|
if (defaultSink?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSink.audio.volume = newValue / 100.0;
|
defaultSink.audio.volume = newValue / 100.0;
|
||||||
if (newValue > 0 && defaultSink.audio.muted) {
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
@@ -97,7 +99,7 @@ Row {
|
|||||||
Binding {
|
Binding {
|
||||||
target: volumeSlider
|
target: volumeSlider
|
||||||
property: "value"
|
property: "value"
|
||||||
value: defaultSink ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
|
value: defaultSink?.audio ? Math.min(AudioService.sinkMaxVolume, Math.round(defaultSink.audio.volume * 100)) : 0
|
||||||
when: !volumeSlider.isDragging
|
when: !volumeSlider.isDragging
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ Row {
|
|||||||
property string instanceId: ""
|
property string instanceId: ""
|
||||||
property string screenName: ""
|
property string screenName: ""
|
||||||
property var parentScreen: null
|
property var parentScreen: null
|
||||||
|
property color sliderTrackColor: "transparent"
|
||||||
|
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
signal iconClicked
|
signal iconClicked
|
||||||
|
|
||||||
@@ -184,7 +186,8 @@ Row {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: root.sliderTrackOpacity
|
||||||
|
|
||||||
Binding on value {
|
Binding on value {
|
||||||
value: root.targetBrightness
|
value: root.targetBrightness
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ Rectangle {
|
|||||||
property real value: 0.0
|
property real value: 0.0
|
||||||
property real maximumValue: 1.0
|
property real maximumValue: 1.0
|
||||||
property real minimumValue: 0.0
|
property real minimumValue: 0.0
|
||||||
property bool enabled: true
|
|
||||||
|
|
||||||
signal sliderValueChanged(real value)
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
width: parent ? parent.width : 200
|
width: parent ? parent.width : 200
|
||||||
height: 60
|
height: 60
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.nestedSurface
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -65,6 +64,8 @@ Rectangle {
|
|||||||
minimum: Math.round(root.minimumValue * 100)
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
maximum: Math.round(root.maximumValue * 100)
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
value: Math.round(root.value * 100)
|
value: Math.round(root.value * 100)
|
||||||
|
trackColor: Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: Theme.ccSliderTrackOpacity
|
||||||
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,23 +29,21 @@ Rectangle {
|
|||||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _containerBg: Theme.ccPillInactiveBg
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
const baseColor = bodyMouse.containsMouse ? Theme.primaryPressed : _containerBg;
|
const baseColor = bodyMouse.containsMouse ? Theme.ccPillInactiveHoverBg : _containerBg;
|
||||||
return baseColor;
|
return baseColor;
|
||||||
}
|
}
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
|
|
||||||
readonly property color _labelPrimary: Theme.surfaceText
|
readonly property color _labelPrimary: Theme.surfaceText
|
||||||
readonly property color _labelSecondary: Theme.surfaceVariantText
|
readonly property color _labelSecondary: Theme.surfaceVariantText
|
||||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||||
readonly property color _tileBgInactive: {
|
readonly property color _tileBgInactive: {
|
||||||
const transparency = Theme.popupTransparency;
|
return Theme.ccTileInactiveBg;
|
||||||
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1);
|
|
||||||
return Qt.rgba(surface.r, surface.g, surface.b, transparency);
|
|
||||||
}
|
}
|
||||||
readonly property color _tileRingActive: Theme.ccTileRing
|
readonly property color _tileRingActive: Theme.ccTileRing
|
||||||
readonly property color _tileRingInactive: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
|
readonly property color _tileRingInactive: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
|
||||||
@@ -92,8 +90,8 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
radius: _tileRadius
|
radius: _tileRadius
|
||||||
color: isActive ? _tileBgActive : _tileBgInactive
|
color: isActive ? _tileBgActive : _tileBgInactive
|
||||||
border.color: isActive ? _tileRingActive : "transparent"
|
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||||
border.width: isActive ? 1 : 0
|
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Row {
|
|||||||
|
|
||||||
property var defaultSource: AudioService.source
|
property var defaultSource: AudioService.source
|
||||||
property color sliderTrackColor: "transparent"
|
property color sliderTrackColor: "transparent"
|
||||||
|
property real sliderTrackOpacity: Theme.ccSliderTrackOpacity
|
||||||
|
|
||||||
height: 40
|
height: 40
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -35,7 +36,7 @@ Row {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => iconRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (defaultSource) {
|
if (defaultSource?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSource.audio.muted = !defaultSource.audio.muted;
|
defaultSource.audio.muted = !defaultSource.audio.muted;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ Row {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
if (!defaultSource)
|
if (!defaultSource?.audio)
|
||||||
return "mic_off";
|
return "mic_off";
|
||||||
|
|
||||||
let volume = defaultSource.audio.volume;
|
let volume = defaultSource.audio.volume;
|
||||||
@@ -56,26 +57,27 @@ Row {
|
|||||||
return "mic";
|
return "mic";
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
color: defaultSource?.audio && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankSlider {
|
DankSlider {
|
||||||
readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0
|
readonly property real actualVolumePercent: defaultSource?.audio ? Math.round(defaultSource.audio.volume * 100) : 0
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
|
||||||
enabled: defaultSource !== null
|
enabled: defaultSource?.audio != null
|
||||||
minimum: 0
|
minimum: 0
|
||||||
maximum: 100
|
maximum: 100
|
||||||
value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
|
value: defaultSource?.audio ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0
|
||||||
showValue: true
|
showValue: true
|
||||||
unit: "%"
|
unit: "%"
|
||||||
valueOverride: actualVolumePercent
|
valueOverride: actualVolumePercent
|
||||||
thumbOutlineColor: Theme.surfaceContainer
|
thumbOutlineColor: Theme.surfaceContainer
|
||||||
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.ccSliderTrackColor
|
||||||
|
trackOpacity: root.sliderTrackOpacity
|
||||||
onSliderValueChanged: function (newValue) {
|
onSliderValueChanged: function (newValue) {
|
||||||
if (defaultSource) {
|
if (defaultSource?.audio) {
|
||||||
SessionData.suppressOSDTemporarily();
|
SessionData.suppressOSDTemporarily();
|
||||||
defaultSource.audio.volume = newValue / 100.0;
|
defaultSource.audio.volume = newValue / 100.0;
|
||||||
if (newValue > 0 && defaultSource.audio.muted) {
|
if (newValue > 0 && defaultSource.audio.muted) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Rectangle {
|
|||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||||
property bool enabled: BatteryService.batteryAvailable
|
enabled: BatteryService.batteryAvailable
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||||
readonly property color _tileRingActive: Theme.ccTileRing
|
readonly property color _tileRingActive: Theme.ccTileRing
|
||||||
readonly property color _tileIconActive: Theme.ccTileActiveText
|
readonly property color _tileIconActive: Theme.ccTileActiveText
|
||||||
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
|
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
|
||||||
@@ -36,11 +36,11 @@ Rectangle {
|
|||||||
color: {
|
color: {
|
||||||
if (isActive)
|
if (isActive)
|
||||||
return _tileBgActive;
|
return _tileBgActive;
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
|
const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
|
||||||
return baseColor;
|
return baseColor;
|
||||||
}
|
}
|
||||||
border.color: isActive ? _tileRingActive : "transparent"
|
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||||
border.width: isActive ? 1 : 0
|
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Rectangle {
|
|||||||
return parseFloat(selectedMount.percent.replace("%", "")) || 0;
|
return parseFloat(selectedMount.percent.replace("%", "")) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool enabled: DgopService.dgopAvailable
|
enabled: DgopService.dgopAvailable
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
|
|
||||||
@@ -38,11 +38,11 @@ Rectangle {
|
|||||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBg: Theme.ccPillInactiveBg
|
||||||
|
|
||||||
color: mouseArea.containsMouse ? Theme.primaryPressed : _tileBg
|
color: mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBg
|
||||||
border.color: "transparent"
|
border.color: Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: Theme.layerOutlineWidth
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ Rectangle {
|
|||||||
|
|
||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property real iconRotation: 0
|
property real iconRotation: 0
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
@@ -27,7 +26,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||||
readonly property color _tileRingActive: Theme.ccTileRing
|
readonly property color _tileRingActive: Theme.ccTileRing
|
||||||
readonly property color _tileIconActive: Theme.ccTileActiveText
|
readonly property color _tileIconActive: Theme.ccTileActiveText
|
||||||
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
|
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
|
||||||
@@ -35,11 +34,11 @@ Rectangle {
|
|||||||
color: {
|
color: {
|
||||||
if (isActive)
|
if (isActive)
|
||||||
return _tileBgActive;
|
return _tileBgActive;
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
|
const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
|
||||||
return baseColor;
|
return baseColor;
|
||||||
}
|
}
|
||||||
border.color: isActive ? _tileRingActive : "transparent"
|
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||||
border.width: isActive ? 1 : 0
|
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Rectangle {
|
|||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property string text: ""
|
property string text: ""
|
||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
property bool enabled: true
|
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property real iconRotation: 0
|
property real iconRotation: 0
|
||||||
|
|
||||||
@@ -27,17 +26,17 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
readonly property color _tileBgActive: Theme.ccTileActiveBg
|
||||||
readonly property color _tileBgInactive: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
|
||||||
readonly property color _tileRingActive: Theme.ccTileRing
|
readonly property color _tileRingActive: Theme.ccTileRing
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
if (isActive)
|
if (isActive)
|
||||||
return _tileBgActive;
|
return _tileBgActive;
|
||||||
const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : _tileBgInactive;
|
const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
|
||||||
return baseColor;
|
return baseColor;
|
||||||
}
|
}
|
||||||
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: isActive ? _tileRingActive : Theme.outlineMedium
|
||||||
border.width: 0
|
border.width: isActive ? 1 : Theme.layerOutlineWidth
|
||||||
opacity: enabled ? 1.0 : 0.6
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
function hoverTint(base) {
|
function hoverTint(base) {
|
||||||
@@ -45,7 +44,7 @@ Rectangle {
|
|||||||
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
readonly property color _containerBg: Theme.ccPillInactiveBg
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0
|
readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0
|
||||||
readonly property bool shadowEnabled: shadowIntensity > 0
|
readonly property bool shadowEnabled: !BlurService.enabled && shadowIntensity > 0
|
||||||
readonly property int blurMax: 64
|
readonly property int blurMax: 64
|
||||||
readonly property real shadowBlurPx: shadowIntensity * 0.2
|
readonly property real shadowBlurPx: shadowIntensity * 0.2
|
||||||
readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax))
|
readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user