mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 19:12:11 -04:00
Compare commits
96 Commits
bd46d29ff0
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa | ||
|
|
dd4c41a6b2 | ||
|
|
92a25fdb6a | ||
|
|
d6650be008 | ||
|
|
2646e7b19a | ||
|
|
4133f11d82 | ||
|
|
22ed740394 | ||
|
|
063299a434 | ||
|
|
44d836c975 | ||
|
|
da437e77fb | ||
|
|
34a6bbfb32 | ||
|
|
9ed53bac9e | ||
|
|
3a6752c3d2 | ||
|
|
ef19568dd7 | ||
|
|
f280cd9d3b | ||
|
|
cf4ce3c476 | ||
|
|
ae59e53c4c | ||
|
|
7e0d661f63 | ||
|
|
0b33d3f905 | ||
|
|
d62bdda56b | ||
|
|
5841b38cd9 | ||
|
|
83e2b5a7a6 | ||
|
|
2f863f64ee | ||
|
|
1a8b397cfd | ||
|
|
196c421b75 | ||
|
|
8399d64c2d | ||
|
|
c530eab303 | ||
|
|
45b6362dd3 | ||
|
|
50b77dcfc3 | ||
|
|
be8f3adf01 | ||
|
|
75a8c171ea | ||
|
|
466ff59573 | ||
|
|
053bb91927 | ||
|
|
2c9b22c016 | ||
|
|
a9ee91586e | ||
|
|
81bce74612 | ||
|
|
f2a6d2c7da | ||
|
|
0a9a34912e | ||
|
|
abff670814 | ||
|
|
0d49acaaa8 | ||
|
|
ebe1785411 | ||
|
|
f9f0192b22 | ||
|
|
e5cdbf4cf5 | ||
|
|
13ef1efa7b | ||
|
|
fbd9301a2d | ||
|
|
24e3024b57 | ||
|
|
52d5af11ba | ||
|
|
44a45b00cf | ||
|
|
2b78fe5b9f | ||
|
|
14f92669c6 | ||
|
|
124106de87 | ||
|
|
bb8e0d384f | ||
|
|
59d37847ec | ||
|
|
acdc531dca | ||
|
|
ce75dac81b | ||
|
|
b8d40761ff | ||
|
|
3a7430f6da | ||
|
|
242660c51d | ||
|
|
8a6c1e45ce | ||
|
|
b8e5f9f3b1 | ||
|
|
d60e70f9cc | ||
|
|
cdb70fadb3 | ||
|
|
7867deef60 | ||
|
|
a77c1adb32 | ||
|
|
da14d75a3b | ||
|
|
7c66a34931 | ||
|
|
425715e0f0 | ||
|
|
a3baf8ce31 | ||
|
|
605e03b065 | ||
|
|
0e9b21d359 | ||
|
|
ba5bf0cabc | ||
|
|
96b9d7aab3 | ||
|
|
750e4c4527 | ||
|
|
7417e26444 | ||
|
|
00e1099912 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -45,9 +45,9 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -v
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -v` command
|
description: Output of `dms doctor -vC` command
|
||||||
placeholder: Paste the output of `dms doctor -v` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/support_request.yml
vendored
6
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -30,9 +30,9 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -v
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -v` command
|
description: Output of `dms doctor -vC` command
|
||||||
placeholder: Paste the output of `dms doctor -v` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -191,6 +191,11 @@ jobs:
|
|||||||
git fetch origin --force tag ${TAG}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -229,6 +234,7 @@ jobs:
|
|||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
|
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||||
- **`dms-qml.tar.gz`** - QML source code only
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
@@ -387,6 +393,19 @@ jobs:
|
|||||||
rm -rf _temp_full
|
rm -rf _temp_full
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Generate vendored source tarball
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
VERSION_NUM=${TAG#v}
|
||||||
|
cd core
|
||||||
|
go mod vendor
|
||||||
|
cd ..
|
||||||
|
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
|
||||||
|
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
|
||||||
|
--exclude='core/.git' \
|
||||||
|
core/
|
||||||
|
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/run-obs.yml
vendored
2
.github/workflows/run-obs.yml
vendored
@@ -335,7 +335,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Install OSC
|
- name: Install OSC
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/run-ppa.yml
vendored
2
.github/workflows/run-ppa.yml
vendored
@@ -158,7 +158,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
|||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||||
|
|
||||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.6.2
|
rev: v2.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
@@ -649,6 +649,109 @@ func checkI2CAvailability() checkResult {
|
|||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkImageFormatPlugins() []checkResult {
|
||||||
|
url := doctorDocsURL + "#optional-features"
|
||||||
|
|
||||||
|
pluginDir := findQtPluginDir()
|
||||||
|
if pluginDir == "" {
|
||||||
|
return []checkResult{
|
||||||
|
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
||||||
|
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||||
|
|
||||||
|
type pluginCheck struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
plugins []struct{ file, format string }
|
||||||
|
}
|
||||||
|
|
||||||
|
checks := []pluginCheck{
|
||||||
|
{
|
||||||
|
name: "qt6-imageformats",
|
||||||
|
desc: "WebP, TIFF, GIF, JP2 support",
|
||||||
|
plugins: []struct{ file, format string }{
|
||||||
|
{"libqwebp.so", "WebP"},
|
||||||
|
{"libqtiff.so", "TIFF"},
|
||||||
|
{"libqgif.so", "GIF"},
|
||||||
|
{"libqjp2.so", "JP2"},
|
||||||
|
{"libqicns.so", "ICNS"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kimageformats",
|
||||||
|
desc: "AVIF, HEIF, JXL support",
|
||||||
|
plugins: []struct{ file, format string }{
|
||||||
|
{"kimg_avif.so", "AVIF"},
|
||||||
|
{"kimg_heif.so", "HEIF"},
|
||||||
|
{"kimg_jxl.so", "JXL"},
|
||||||
|
{"kimg_exr.so", "EXR"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
for _, c := range checks {
|
||||||
|
var found []string
|
||||||
|
for _, p := range c.plugins {
|
||||||
|
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||||
|
found = append(found, p.format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result checkResult
|
||||||
|
switch {
|
||||||
|
case len(found) == 0:
|
||||||
|
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
|
||||||
|
default:
|
||||||
|
details := ""
|
||||||
|
if doctorVerbose {
|
||||||
|
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
|
||||||
|
}
|
||||||
|
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func findQtPluginDir() string {
|
||||||
|
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
||||||
|
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||||
|
for dir := range strings.SplitSeq(envPath, ":") {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try qtpaths
|
||||||
|
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||||
|
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||||
|
if dir := strings.TrimSpace(string(output)); dir != "" {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: common distro paths
|
||||||
|
for _, dir := range []string{
|
||||||
|
"/usr/lib/qt6/plugins",
|
||||||
|
"/usr/lib64/qt6/plugins",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||||
|
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
||||||
|
} {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
switch stackResult.Backend {
|
switch stackResult.Backend {
|
||||||
case network.BackendNetworkManager:
|
case network.BackendNetworkManager:
|
||||||
@@ -689,7 +792,21 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
||||||
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
||||||
|
|
||||||
|
cupsPkHelperBus := "org.opensuse.CupsPkHelper.Mechanism"
|
||||||
|
var cupsPkStatus status
|
||||||
|
var cupsPkMsg string
|
||||||
|
switch {
|
||||||
|
case utils.IsDBusServiceAvailable(cupsPkHelperBus):
|
||||||
|
cupsPkStatus, cupsPkMsg = statusOK, "Running"
|
||||||
|
case utils.IsDBusServiceActivatable(cupsPkHelperBus):
|
||||||
|
cupsPkStatus, cupsPkMsg = statusOK, "Available"
|
||||||
|
default:
|
||||||
|
cupsPkStatus, cupsPkMsg = statusWarn, "Not available (install cups-pk-helper)"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
|
||||||
|
|
||||||
results = append(results, checkI2CAvailability())
|
results = append(results, checkI2CAvailability())
|
||||||
|
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 {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ func syncGreeter() error {
|
|||||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterGroupExists := checkGroupExists("greeter")
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
|
greeterGroupExists := utils.HasGroup(greeterGroup)
|
||||||
if greeterGroupExists {
|
if greeterGroupExists {
|
||||||
currentUser, err := user.Current()
|
currentUser, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,25 +183,27 @@ func syncGreeter() error {
|
|||||||
return fmt.Errorf("failed to check groups: %w", err)
|
return fmt.Errorf("failed to check groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||||
if !inGreeterGroup {
|
if !inGreeterGroup {
|
||||||
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
|
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||||
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
|
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
if response == "y" || response == "yes" {
|
if response != "n" && response != "no" {
|
||||||
fmt.Println("\nAdding user to greeter group...")
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
|
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||||
addUserCmd.Stdout = os.Stdout
|
addUserCmd.Stdout = os.Stdout
|
||||||
addUserCmd.Stderr = os.Stderr
|
addUserCmd.Stderr = os.Stderr
|
||||||
if err := addUserCmd.Run(); err != nil {
|
if err := addUserCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to add user to greeter group: %w", err)
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
}
|
}
|
||||||
fmt.Println("✓ User added to greeter group")
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,21 +246,6 @@ func syncGreeter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGroupExists(groupName string) bool {
|
|
||||||
data, err := os.ReadFile("/etc/group")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.SplitSeq(string(data), "\n")
|
|
||||||
for line := range lines {
|
|
||||||
if strings.HasPrefix(line, groupName+":") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func disableDisplayManager(dmName string) (bool, error) {
|
func disableDisplayManager(dmName string) (bool, error) {
|
||||||
state, err := getSystemdServiceState(dmName)
|
state, err := getSystemdServiceState(dmName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,7 +377,7 @@ func ensureGraphicalTarget() error {
|
|||||||
func handleConflictingDisplayManagers() error {
|
func handleConflictingDisplayManagers() error {
|
||||||
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
||||||
|
|
||||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
|
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
|
|
||||||
disabledAny := false
|
disabledAny := false
|
||||||
var errors []string
|
var errors []string
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||||
@@ -82,24 +83,35 @@ func init() {
|
|||||||
func initializeProviders() {
|
func initializeProviders() {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
hyprlandProvider := providers.NewHyprlandProvider("")
|
||||||
if err := registry.Register(hyprlandProvider); err != nil {
|
if err := registry.Register(hyprlandProvider); err != nil {
|
||||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
|
mangowcProvider := providers.NewMangoWCProvider("")
|
||||||
if err := registry.Register(mangowcProvider); err != nil {
|
if err := registry.Register(mangowcProvider); err != nil {
|
||||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
|
configDir, _ := os.UserConfigDir()
|
||||||
if err := registry.Register(scrollProvider); err != nil {
|
|
||||||
log.Warnf("Failed to register Scroll provider: %v", err)
|
if configDir != "" {
|
||||||
|
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
|
||||||
|
if err := registry.Register(scrollProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Scroll provider: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
miracleProvider := providers.NewMiracleProvider("")
|
||||||
if err := registry.Register(swayProvider); err != nil {
|
if err := registry.Register(miracleProvider); err != nil {
|
||||||
log.Warnf("Failed to register Sway provider: %v", err)
|
log.Warnf("Failed to register Miracle WM provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configDir != "" {
|
||||||
|
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
|
||||||
|
if err := registry.Register(swayProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Sway provider: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
niriProvider := providers.NewNiriProvider("")
|
niriProvider := providers.NewNiriProvider("")
|
||||||
@@ -144,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
|
|||||||
return providers.NewSwayProvider(path)
|
return providers.NewSwayProvider(path)
|
||||||
case "scroll":
|
case "scroll":
|
||||||
return providers.NewSwayProvider(path)
|
return providers.NewSwayProvider(path)
|
||||||
|
case "miracle":
|
||||||
|
return providers.NewMiracleProvider(path)
|
||||||
case "niri":
|
case "niri":
|
||||||
return providers.NewNiriProvider(path)
|
return providers.NewNiriProvider(path)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ssOutputName string
|
ssOutputName string
|
||||||
ssIncludeCursor bool
|
ssCursor string
|
||||||
ssFormat string
|
ssFormat string
|
||||||
ssQuality int
|
ssQuality int
|
||||||
ssOutputDir string
|
ssOutputDir string
|
||||||
ssFilename string
|
ssFilename string
|
||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
@@ -52,7 +52,7 @@ Examples:
|
|||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
dms screenshot --no-clipboard # Save file only
|
dms screenshot --no-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --cursor # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||||
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||||
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config := screenshot.DefaultConfig()
|
config := screenshot.DefaultConfig()
|
||||||
config.Mode = mode
|
config.Mode = mode
|
||||||
config.OutputName = ssOutputName
|
config.OutputName = ssOutputName
|
||||||
config.IncludeCursor = ssIncludeCursor
|
if strings.EqualFold(ssCursor, "on") {
|
||||||
|
config.Cursor = screenshot.CursorOn
|
||||||
|
}
|
||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
}
|
}
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
@@ -450,7 +450,7 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
}
|
}
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||||
}
|
}
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
|
|||||||
39
core/go.mod
39
core/go.mod
@@ -1,11 +1,11 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.24.6
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
@@ -19,44 +19,43 @@ require (
|
|||||||
github.com/yuin/goldmark v1.7.16
|
github.com/yuin/goldmark v1.7.16
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||||
golang.org/x/image v0.35.0
|
golang.org/x/image v0.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.8.0 // indirect
|
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -70,7 +69,11 @@ require (
|
|||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.40.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.33.0
|
golang.org/x/text v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// v0.0.1 tag is missing a LICENSE file; master has it.
|
||||||
|
// See: https://github.com/mattn/go-localereader/issues/2
|
||||||
|
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75
|
||||||
|
|||||||
70
core/go.sum
70
core/go.sum
@@ -20,30 +20,28 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.8.0 h1:/z8v+H+4XLluJKS7rAc7uHZTalT5Z+1430ld3lePSRI=
|
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||||
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ=
|
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -66,12 +64,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 h1:VzdR70t+SMjYnBgnbtNpq4ElZAAovLPMG+GFX8OBRtM=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -88,8 +86,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
|
|||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -103,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -152,24 +150,24 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.m
|
|||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
||||||
startupSectionFound = true
|
startupSectionFound = true
|
||||||
result = append(result, "exec-once = dms run")
|
result = append(result, "exec-once = dms run")
|
||||||
result = append(result, "env = QT_QPA_PLATFORM,wayland")
|
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
|
||||||
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
||||||
@@ -659,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
if strings.Contains(line, "STARTUP APPS") {
|
if strings.Contains(line, "STARTUP APPS") {
|
||||||
insertLines := []string{
|
insertLines := []string{
|
||||||
"exec-once = dms run",
|
"exec-once = dms run",
|
||||||
"env = QT_QPA_PLATFORM,wayland",
|
"env = QT_QPA_PLATFORM,wayland;xcb",
|
||||||
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
||||||
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
||||||
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
||||||
@@ -677,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
envVars := fmt.Sprintf(`environment {
|
envVars := fmt.Sprintf(`environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
QT_QPA_PLATFORM "wayland"
|
QT_QPA_PLATFORM "wayland;xcb"
|
||||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
QT_QPA_PLATFORMTHEME "gtk3"
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
|
|||||||
# windowrule = float on, match:class ^(org.quickshell)$
|
# windowrule = float on, match:class ^(org.quickshell)$
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
|
layerrule = no_anim on, match:namespace ^dms:.*
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
source = ./dms/colors.conf
|
||||||
source = ./dms/outputs.conf
|
source = ./dms/outputs.conf
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
|
|||||||
@@ -22,6 +22,21 @@ func DetectDMSPath() (string, error) {
|
|||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DetectGreeterGroup() string {
|
||||||
|
data, err := os.ReadFile("/etc/group")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter")
|
||||||
|
return "greeter"
|
||||||
|
}
|
||||||
|
|
||||||
|
if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter")
|
||||||
|
return "greeter"
|
||||||
|
}
|
||||||
|
|
||||||
// DetectCompositors checks which compositors are installed
|
// DetectCompositors checks which compositors are installed
|
||||||
func DetectCompositors() []string {
|
func DetectCompositors() []string {
|
||||||
var compositors []string
|
var compositors []string
|
||||||
@@ -194,14 +209,17 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil {
|
group := DetectGreeterGroup()
|
||||||
|
owner := fmt.Sprintf("%s:%s", group, group)
|
||||||
|
|
||||||
|
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 755)", cacheDir))
|
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -234,6 +252,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
owner := DetectGreeterGroup()
|
||||||
|
|
||||||
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
||||||
|
|
||||||
for _, dir := range parentDirs {
|
for _, dir := range parentDirs {
|
||||||
@@ -245,9 +265,9 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set ACL to allow greeter user read+execute permission (for session discovery)
|
// Set ACL to allow greeter user read+execute permission (for session discovery)
|
||||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
|
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
return fmt.Errorf("failed to determine current user")
|
return fmt.Errorf("failed to determine current user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group := DetectGreeterGroup()
|
||||||
|
|
||||||
// Check if user is already in greeter group
|
// Check if user is already in greeter group
|
||||||
groupsCmd := exec.Command("groups", currentUser)
|
groupsCmd := exec.Command("groups", currentUser)
|
||||||
groupsOutput, err := groupsCmd.Output()
|
groupsOutput, err := groupsCmd.Output()
|
||||||
if err == nil && strings.Contains(string(groupsOutput), "greeter") {
|
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||||
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser))
|
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||||
} else {
|
} else {
|
||||||
// Add current user to greeter group for file access permissions
|
// Add current user to greeter group for file access permissions
|
||||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil {
|
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||||
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err)
|
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser))
|
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||||
}
|
}
|
||||||
|
|
||||||
configDirs := []struct {
|
configDirs := []struct {
|
||||||
@@ -304,7 +326,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil {
|
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -436,10 +458,11 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
greeterDir := "/etc/greetd/niri"
|
greeterDir := "/etc/greetd/niri"
|
||||||
|
greeterGroup := DetectGreeterGroup()
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chown", "root:greeter", greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||||
@@ -464,7 +487,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +510,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
|||||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||||
}
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,17 +759,19 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw
|
|||||||
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
greeterUser := DetectGreeterGroup()
|
||||||
|
|
||||||
var configContent string
|
var configContent string
|
||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent = string(data)
|
configContent = string(data)
|
||||||
} else {
|
} else {
|
||||||
configContent = `[terminal]
|
configContent = fmt.Sprintf(`[terminal]
|
||||||
vt = 1
|
vt = 1
|
||||||
|
|
||||||
[default_session]
|
[default_session]
|
||||||
|
|
||||||
user = "greeter"
|
user = "%s"
|
||||||
`
|
`, greeterUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(configContent, "\n")
|
lines := strings.Split(configContent, "\n")
|
||||||
@@ -755,7 +780,7 @@ user = "greeter"
|
|||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||||
newLines = append(newLines, `user = "greeter"`)
|
newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
|
||||||
} else {
|
} else {
|
||||||
newLines = append(newLines, line)
|
newLines = append(newLines, line)
|
||||||
}
|
}
|
||||||
@@ -807,7 +832,7 @@ user = "greeter"
|
|||||||
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath))
|
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
95
core/internal/keybinds/providers/miracle.go
Normal file
95
core/internal/keybinds/providers/miracle.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiracleProvider struct {
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiracleProvider(configPath string) *MiracleProvider {
|
||||||
|
if configPath == "" {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err == nil {
|
||||||
|
configPath = filepath.Join(configDir, "miracle-wm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &MiracleProvider{configPath: configPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) Name() string {
|
||||||
|
return "miracle"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
|
config, err := ParseMiracleConfig(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings := MiracleConfigToBindings(config)
|
||||||
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
|
|
||||||
|
for _, kb := range bindings {
|
||||||
|
category := m.categorizeAction(kb.Action)
|
||||||
|
bind := keybinds.Keybind{
|
||||||
|
Key: m.formatKey(kb),
|
||||||
|
Description: kb.Comment,
|
||||||
|
Action: kb.Action,
|
||||||
|
}
|
||||||
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keybinds.CheatSheet{
|
||||||
|
Title: "Miracle WM Keybinds",
|
||||||
|
Provider: m.Name(),
|
||||||
|
Binds: categorizedBinds,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) GetOverridePath() string {
|
||||||
|
expanded, err := utils.ExpandPath(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.Join(m.configPath, "config.yaml")
|
||||||
|
}
|
||||||
|
return filepath.Join(expanded, "config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) categorizeAction(action string) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
|
||||||
|
return "Workspace"
|
||||||
|
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
|
||||||
|
return "Window"
|
||||||
|
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
|
||||||
|
return "Window"
|
||||||
|
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
|
||||||
|
return "Window"
|
||||||
|
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
|
||||||
|
return "Layout"
|
||||||
|
case action == "quit_compositor":
|
||||||
|
return "System"
|
||||||
|
case action == "terminal":
|
||||||
|
return "Execute"
|
||||||
|
case strings.HasPrefix(action, "magnifier_"):
|
||||||
|
return "Accessibility"
|
||||||
|
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
|
||||||
|
return "Execute"
|
||||||
|
default:
|
||||||
|
return "Execute"
|
||||||
|
}
|
||||||
|
}
|
||||||
320
core/internal/keybinds/providers/miracle_parser.go
Normal file
320
core/internal/keybinds/providers/miracle_parser.go
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiracleConfig struct {
|
||||||
|
Terminal string `yaml:"terminal"`
|
||||||
|
ActionKey string `yaml:"action_key"`
|
||||||
|
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
|
||||||
|
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleActionOverride struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Action string `yaml:"action"`
|
||||||
|
Modifiers []string `yaml:"modifiers"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleCustomAction struct {
|
||||||
|
Command string `yaml:"command"`
|
||||||
|
Action string `yaml:"action"`
|
||||||
|
Modifiers []string `yaml:"modifiers"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleKeyBinding struct {
|
||||||
|
Mods []string
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
var miracleDefaultBinds = []MiracleKeyBinding{
|
||||||
|
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
|
||||||
|
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
|
||||||
|
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
|
||||||
|
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
|
||||||
|
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
|
||||||
|
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
|
||||||
|
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
|
||||||
|
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
|
||||||
|
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
|
||||||
|
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
|
||||||
|
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
|
||||||
|
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
|
||||||
|
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
|
||||||
|
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
|
||||||
|
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
|
||||||
|
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
|
||||||
|
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
|
||||||
|
expanded, err := utils.ExpandPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(expanded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFile string
|
||||||
|
if info.IsDir() {
|
||||||
|
configFile = filepath.Join(expanded, "config.yaml")
|
||||||
|
} else {
|
||||||
|
configFile = expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config MiracleConfig
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ActionKey == "" {
|
||||||
|
config.ActionKey = "meta"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMiracleModifier(mod, actionKey string) string {
|
||||||
|
switch mod {
|
||||||
|
case "primary":
|
||||||
|
return resolveActionKey(actionKey)
|
||||||
|
case "alt", "alt_left", "alt_right":
|
||||||
|
return "Alt"
|
||||||
|
case "shift", "shift_left", "shift_right":
|
||||||
|
return "Shift"
|
||||||
|
case "ctrl", "ctrl_left", "ctrl_right":
|
||||||
|
return "Ctrl"
|
||||||
|
case "meta", "meta_left", "meta_right":
|
||||||
|
return "Super"
|
||||||
|
default:
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveActionKey(actionKey string) string {
|
||||||
|
switch actionKey {
|
||||||
|
case "meta":
|
||||||
|
return "Super"
|
||||||
|
case "alt":
|
||||||
|
return "Alt"
|
||||||
|
case "ctrl":
|
||||||
|
return "Ctrl"
|
||||||
|
default:
|
||||||
|
return "Super"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func miracleKeyCodeToName(keyCode string) string {
|
||||||
|
name := strings.TrimPrefix(keyCode, "KEY_")
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "enter":
|
||||||
|
return "Return"
|
||||||
|
case "space":
|
||||||
|
return "Space"
|
||||||
|
case "up":
|
||||||
|
return "Up"
|
||||||
|
case "down":
|
||||||
|
return "Down"
|
||||||
|
case "left":
|
||||||
|
return "Left"
|
||||||
|
case "right":
|
||||||
|
return "Right"
|
||||||
|
case "tab":
|
||||||
|
return "Tab"
|
||||||
|
case "escape", "esc":
|
||||||
|
return "Escape"
|
||||||
|
case "delete":
|
||||||
|
return "Delete"
|
||||||
|
case "backspace":
|
||||||
|
return "BackSpace"
|
||||||
|
case "home":
|
||||||
|
return "Home"
|
||||||
|
case "end":
|
||||||
|
return "End"
|
||||||
|
case "pageup":
|
||||||
|
return "Page_Up"
|
||||||
|
case "pagedown":
|
||||||
|
return "Page_Down"
|
||||||
|
case "print":
|
||||||
|
return "Print"
|
||||||
|
case "pause":
|
||||||
|
return "Pause"
|
||||||
|
case "volumeup":
|
||||||
|
return "XF86AudioRaiseVolume"
|
||||||
|
case "volumedown":
|
||||||
|
return "XF86AudioLowerVolume"
|
||||||
|
case "mute":
|
||||||
|
return "XF86AudioMute"
|
||||||
|
case "micmute":
|
||||||
|
return "XF86AudioMicMute"
|
||||||
|
case "brightnessup":
|
||||||
|
return "XF86MonBrightnessUp"
|
||||||
|
case "brightnessdown":
|
||||||
|
return "XF86MonBrightnessDown"
|
||||||
|
case "kbdillumup":
|
||||||
|
return "XF86KbdBrightnessUp"
|
||||||
|
case "kbdillumdown":
|
||||||
|
return "XF86KbdBrightnessDown"
|
||||||
|
case "comma":
|
||||||
|
return "comma"
|
||||||
|
case "minus":
|
||||||
|
return "minus"
|
||||||
|
case "equal":
|
||||||
|
return "equal"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) == 1 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
|
||||||
|
overridden := make(map[string]bool)
|
||||||
|
var bindings []MiracleKeyBinding
|
||||||
|
|
||||||
|
for _, override := range config.DefaultActionOverrides {
|
||||||
|
mods := make([]string, 0, len(override.Modifiers))
|
||||||
|
for _, mod := range override.Modifiers {
|
||||||
|
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings = append(bindings, MiracleKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: miracleKeyCodeToName(override.Key),
|
||||||
|
Action: override.Name,
|
||||||
|
Comment: miracleActionDescription(override.Name),
|
||||||
|
})
|
||||||
|
overridden[override.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, def := range miracleDefaultBinds {
|
||||||
|
if overridden[def.Action] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bindings = append(bindings, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, custom := range config.CustomActions {
|
||||||
|
mods := make([]string, 0, len(custom.Modifiers))
|
||||||
|
for _, mod := range custom.Modifiers {
|
||||||
|
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings = append(bindings, MiracleKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: miracleKeyCodeToName(custom.Key),
|
||||||
|
Action: custom.Command,
|
||||||
|
Comment: custom.Command,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func miracleActionDescription(action string) string {
|
||||||
|
switch action {
|
||||||
|
case "terminal":
|
||||||
|
return "Open terminal"
|
||||||
|
case "request_vertical":
|
||||||
|
return "Layout windows vertically"
|
||||||
|
case "request_horizontal":
|
||||||
|
return "Layout windows horizontally"
|
||||||
|
case "select_up":
|
||||||
|
return "Select window above"
|
||||||
|
case "select_down":
|
||||||
|
return "Select window below"
|
||||||
|
case "select_left":
|
||||||
|
return "Select window left"
|
||||||
|
case "select_right":
|
||||||
|
return "Select window right"
|
||||||
|
case "move_up":
|
||||||
|
return "Move window up"
|
||||||
|
case "move_down":
|
||||||
|
return "Move window down"
|
||||||
|
case "move_left":
|
||||||
|
return "Move window left"
|
||||||
|
case "move_right":
|
||||||
|
return "Move window right"
|
||||||
|
case "toggle_resize":
|
||||||
|
return "Toggle resize mode"
|
||||||
|
case "fullscreen":
|
||||||
|
return "Toggle fullscreen"
|
||||||
|
case "quit_active_window":
|
||||||
|
return "Close window"
|
||||||
|
case "quit_compositor":
|
||||||
|
return "Exit compositor"
|
||||||
|
case "toggle_floating":
|
||||||
|
return "Toggle floating"
|
||||||
|
case "toggle_pinned_to_workspace":
|
||||||
|
return "Toggle pinned to workspace"
|
||||||
|
case "toggle_tabbing":
|
||||||
|
return "Toggle tabbing layout"
|
||||||
|
case "toggle_stacking":
|
||||||
|
return "Toggle stacking layout"
|
||||||
|
case "magnifier_on":
|
||||||
|
return "Enable magnifier"
|
||||||
|
case "magnifier_off":
|
||||||
|
return "Disable magnifier"
|
||||||
|
case "magnifier_increase_size":
|
||||||
|
return "Increase magnifier area"
|
||||||
|
case "magnifier_decrease_size":
|
||||||
|
return "Decrease magnifier area"
|
||||||
|
case "magnifier_increase_scale":
|
||||||
|
return "Increase magnifier scale"
|
||||||
|
case "magnifier_decrease_scale":
|
||||||
|
return "Decrease magnifier scale"
|
||||||
|
}
|
||||||
|
|
||||||
|
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
|
||||||
|
return "Workspace " + num
|
||||||
|
}
|
||||||
|
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
|
||||||
|
return "Move to workspace " + num
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
|||||||
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
||||||
|
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
configDir = ""
|
||||||
|
}
|
||||||
if scrollEnvSet {
|
if scrollEnvSet {
|
||||||
configPath = "$HOME/.config/scroll"
|
if configDir != "" {
|
||||||
|
configPath = filepath.Join(configDir, "scroll")
|
||||||
|
}
|
||||||
isScroll = true
|
isScroll = true
|
||||||
} else {
|
} else {
|
||||||
configPath = "$HOME/.config/sway"
|
if configDir != "" {
|
||||||
|
configPath = filepath.Join(configDir, "sway")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Determine isScroll based on the provided config path
|
|
||||||
isScroll = strings.Contains(configPath, "scroll")
|
isScroll = strings.Contains(configPath, "scroll")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SwayProvider) Name() string {
|
func (s *SwayProvider) Name() string {
|
||||||
if s != nil && s.isScroll {
|
|
||||||
return "scroll"
|
|
||||||
}
|
|
||||||
if s == nil {
|
if s == nil {
|
||||||
_, ok := os.LookupEnv("SCROLLSOCK")
|
if os.Getenv("SCROLLSOCK") != "" {
|
||||||
if ok {
|
|
||||||
return "scroll"
|
return "scroll"
|
||||||
}
|
}
|
||||||
|
return "sway"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.isScroll {
|
||||||
|
return "scroll"
|
||||||
|
}
|
||||||
return "sway"
|
return "sway"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
|
|||||||
|
|
||||||
func TestSwayProviderDefaultPath(t *testing.T) {
|
func TestSwayProviderDefaultPath(t *testing.T) {
|
||||||
provider := NewSwayProvider("")
|
provider := NewSwayProvider("")
|
||||||
if provider.configPath != "$HOME/.config/sway" {
|
configDir, err := os.UserConfigDir()
|
||||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
|
if err != nil {
|
||||||
|
t.Skip("UserConfigDir not available")
|
||||||
|
}
|
||||||
|
expected := filepath.Join(configDir, "sway")
|
||||||
|
if provider.configPath != expected {
|
||||||
|
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const (
|
|||||||
TemplateKindTerminal
|
TemplateKindTerminal
|
||||||
TemplateKindGTK
|
TemplateKindGTK
|
||||||
TemplateKindVSCode
|
TemplateKindVSCode
|
||||||
|
TemplateKindEmacs
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateDef struct {
|
type TemplateDef struct {
|
||||||
@@ -65,7 +66,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -78,8 +79,10 @@ func (c *ColorMode) GTKTheme() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
matugenVersionOnce sync.Once
|
matugenVersionMu sync.Mutex
|
||||||
|
matugenVersionOK bool
|
||||||
matugenSupportsCOE bool
|
matugenSupportsCOE bool
|
||||||
|
matugenIsV4 bool
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -268,7 +271,7 @@ func buildOnce(opts *Options) error {
|
|||||||
refreshQt6ct()
|
refreshQt6ct()
|
||||||
}
|
}
|
||||||
|
|
||||||
signalTerminals()
|
signalTerminals(opts)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -333,6 +336,10 @@ output_path = '%s'
|
|||||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||||
|
case TemplateKindEmacs:
|
||||||
|
if utils.EmacsConfigDir() != "" {
|
||||||
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||||
}
|
}
|
||||||
@@ -490,6 +497,9 @@ func substituteVars(content, shellDir string) string {
|
|||||||
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
||||||
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
||||||
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
||||||
|
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
|
||||||
|
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,67 +520,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
|
|||||||
return content[startIdx : startIdx+endIdx]
|
return content[startIdx : startIdx+endIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMatugenVersion() {
|
type matugenFlags struct {
|
||||||
matugenVersionOnce.Do(func() {
|
supportsCOE bool
|
||||||
cmd := exec.Command("matugen", "--version")
|
isV4 bool
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStr := strings.TrimSpace(string(output))
|
|
||||||
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
|
||||||
|
|
||||||
parts := strings.Split(versionStr, ".")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
major, err := strconv.Atoi(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
minor, err := strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
|
||||||
if matugenSupportsCOE {
|
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugen(args []string) error {
|
func detectMatugenVersion() (matugenFlags, error) {
|
||||||
checkMatugenVersion()
|
matugenVersionMu.Lock()
|
||||||
|
defer matugenVersionMu.Unlock()
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenVersionOK {
|
||||||
args = append([]string{"--continue-on-error"}, args...)
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return detectMatugenVersionLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
|
||||||
|
matugenVersionMu.Lock()
|
||||||
|
defer matugenVersionMu.Unlock()
|
||||||
|
|
||||||
|
matugenVersionOK = false
|
||||||
|
flags, err := detectMatugenVersionLocked()
|
||||||
|
if err != nil {
|
||||||
|
return old, false
|
||||||
|
}
|
||||||
|
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
|
||||||
|
return flags, changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||||
|
cmd := exec.Command("matugen", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := strings.TrimSpace(string(output))
|
||||||
|
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||||
|
|
||||||
|
parts := strings.Split(versionStr, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||||
|
matugenIsV4 = major >= 4
|
||||||
|
matugenVersionOK = true
|
||||||
|
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||||
|
}
|
||||||
|
if matugenIsV4 {
|
||||||
|
log.Infof("Matugen %s: using v4 flags", versionStr)
|
||||||
|
}
|
||||||
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
|
||||||
|
args := make([]string, 0, len(baseArgs)+4)
|
||||||
|
if flags.supportsCOE {
|
||||||
|
args = append(args, "--continue-on-error")
|
||||||
|
}
|
||||||
|
args = append(args, baseArgs...)
|
||||||
|
if flags.isV4 {
|
||||||
|
args = append(args, "--source-color-index", "0")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugen(baseArgs []string) error {
|
||||||
|
flags, err := detectMatugenVersion()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := buildMatugenArgs(baseArgs, flags)
|
||||||
cmd := exec.Command("matugen", args...)
|
cmd := exec.Command("matugen", args...)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
runErr := cmd.Run()
|
||||||
|
if runErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
|
||||||
|
|
||||||
|
newFlags, changed := redetectMatugenVersion(flags)
|
||||||
|
if !changed {
|
||||||
|
return runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
|
||||||
|
args = buildMatugenArgs(baseArgs, newFlags)
|
||||||
|
retryCmd := exec.Command("matugen", args...)
|
||||||
|
retryCmd.Stdout = os.Stdout
|
||||||
|
retryCmd.Stderr = os.Stderr
|
||||||
|
return retryCmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugenDryRun(opts *Options) (string, error) {
|
func runMatugenDryRun(opts *Options) (string, error) {
|
||||||
var args []string
|
flags, err := detectMatugenVersion()
|
||||||
switch opts.Kind {
|
|
||||||
case "hex":
|
|
||||||
args = []string{"color", "hex", opts.Value}
|
|
||||||
default:
|
|
||||||
args = []string{opts.Kind, opts.Value}
|
|
||||||
}
|
|
||||||
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
|
||||||
|
|
||||||
cmd := exec.Command("matugen", args...)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output, dryErr := execDryRun(opts, flags)
|
||||||
|
if dryErr == nil {
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
|
||||||
|
|
||||||
|
newFlags, changed := redetectMatugenVersion(flags)
|
||||||
|
if !changed {
|
||||||
|
return "", dryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
|
||||||
|
return execDryRun(opts, newFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||||
|
var baseArgs []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
baseArgs = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
baseArgs = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
if flags.isV4 {
|
||||||
|
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", baseArgs...)
|
||||||
|
var stderr strings.Builder
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if stderr.Len() > 0 {
|
||||||
|
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
|
||||||
|
}
|
||||||
return strings.ReplaceAll(string(output), "\n", ""), nil
|
return strings.ReplaceAll(string(output), "\n", ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,11 +795,15 @@ func refreshQt6ct() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalTerminals() {
|
func signalTerminals(opts *Options) {
|
||||||
signalByName("kitty", syscall.SIGUSR1)
|
if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) {
|
||||||
signalByName("ghostty", syscall.SIGUSR2)
|
signalByName("kitty", syscall.SIGUSR1)
|
||||||
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
||||||
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("ghostty") && appExists(opts.AppChecker, []string{"ghostty"}, nil) {
|
||||||
|
signalByName("ghostty", syscall.SIGUSR2)
|
||||||
|
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalByName(name string, sig syscall.Signal) {
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
@@ -802,6 +909,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
|||||||
detected = true
|
detected = true
|
||||||
case tmpl.Kind == TemplateKindVSCode:
|
case tmpl.Kind == TemplateKindVSCode:
|
||||||
detected = checkVSCodeExtension(homeDir)
|
detected = checkVSCodeExtension(homeDir)
|
||||||
|
case tmpl.Kind == TemplateKindEmacs:
|
||||||
|
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
|
||||||
default:
|
default:
|
||||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ const (
|
|||||||
notifyDest = "org.freedesktop.Notifications"
|
notifyDest = "org.freedesktop.Notifications"
|
||||||
notifyPath = "/org/freedesktop/Notifications"
|
notifyPath = "/org/freedesktop/Notifications"
|
||||||
notifyInterface = "org.freedesktop.Notifications"
|
notifyInterface = "org.freedesktop.Notifications"
|
||||||
|
|
||||||
|
maxSummaryLen = 29
|
||||||
|
maxBodyLen = 80
|
||||||
)
|
)
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
@@ -39,6 +42,13 @@ func Send(n Notification) error {
|
|||||||
n.Timeout = 5000
|
n.Timeout = 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(n.Summary) > maxSummaryLen {
|
||||||
|
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
|
||||||
|
}
|
||||||
|
if len(n.Body) > maxBodyLen {
|
||||||
|
n.Body = n.Body[:maxBodyLen-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
var actions []string
|
var actions []string
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
actions = []string{
|
actions = []string{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
CompositorNiri
|
CompositorNiri
|
||||||
CompositorDWL
|
CompositorDWL
|
||||||
CompositorScroll
|
CompositorScroll
|
||||||
|
CompositorMiracle
|
||||||
)
|
)
|
||||||
|
|
||||||
var detectedCompositor Compositor = -1
|
var detectedCompositor Compositor = -1
|
||||||
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
|
|||||||
niriSocket := os.Getenv("NIRI_SOCKET")
|
niriSocket := os.Getenv("NIRI_SOCKET")
|
||||||
swaySocket := os.Getenv("SWAYSOCK")
|
swaySocket := os.Getenv("SWAYSOCK")
|
||||||
scrollSocket := os.Getenv("SCROLLSOCK")
|
scrollSocket := os.Getenv("SCROLLSOCK")
|
||||||
|
miracleSocket := os.Getenv("MIRACLESOCK")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case niriSocket != "":
|
case niriSocket != "":
|
||||||
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
|
|||||||
detectedCompositor = CompositorScroll
|
detectedCompositor = CompositorScroll
|
||||||
return detectedCompositor
|
return detectedCompositor
|
||||||
}
|
}
|
||||||
|
case miracleSocket != "":
|
||||||
|
if _, err := os.Stat(miracleSocket); err == nil {
|
||||||
|
detectedCompositor = CompositorMiracle
|
||||||
|
return detectedCompositor
|
||||||
|
}
|
||||||
case swaySocket != "":
|
case swaySocket != "":
|
||||||
if _, err := os.Stat(swaySocket); err == nil {
|
if _, err := os.Stat(swaySocket); err == nil {
|
||||||
detectedCompositor = CompositorSway
|
detectedCompositor = CompositorSway
|
||||||
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getMiracleFocusedMonitor() string {
|
||||||
|
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var workspaces []swayWorkspace
|
||||||
|
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
if ws.Focused {
|
||||||
|
return ws.Output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type niriWorkspace struct {
|
type niriWorkspace struct {
|
||||||
Output string `json:"output"`
|
Output string `json:"output"`
|
||||||
IsFocused bool `json:"is_focused"`
|
IsFocused bool `json:"is_focused"`
|
||||||
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
|
|||||||
return getSwayFocusedMonitor()
|
return getSwayFocusedMonitor()
|
||||||
case CompositorScroll:
|
case CompositorScroll:
|
||||||
return getScrollFocusedMonitor()
|
return getScrollFocusedMonitor()
|
||||||
|
case CompositorMiracle:
|
||||||
|
return getMiracleFocusedMonitor()
|
||||||
case CompositorNiri:
|
case CompositorNiri:
|
||||||
return getNiriFocusedMonitor()
|
return getNiriFocusedMonitor()
|
||||||
case CompositorDWL:
|
case CompositorDWL:
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
|||||||
screenshoter: s,
|
screenshoter: s,
|
||||||
outputs: make(map[uint32]*WaylandOutput),
|
outputs: make(map[uint32]*WaylandOutput),
|
||||||
preCapture: make(map[*WaylandOutput]*PreCapture),
|
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||||
showCapturedCursor: true,
|
showCapturedCursor: s.config.Cursor == CursorOn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
||||||
cursor := int32(0)
|
cursor := int32(s.config.Cursor)
|
||||||
if s.config.IncludeCursor {
|
|
||||||
cursor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor := int32(0)
|
cursor := int32(s.config.Cursor)
|
||||||
if s.config.IncludeCursor {
|
|
||||||
cursor = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
|
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ const (
|
|||||||
FormatPPM
|
FormatPPM
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CursorMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CursorOff CursorMode = iota
|
||||||
|
CursorOn
|
||||||
|
)
|
||||||
|
|
||||||
type Region struct {
|
type Region struct {
|
||||||
X int32 `json:"x"`
|
X int32 `json:"x"`
|
||||||
Y int32 `json:"y"`
|
Y int32 `json:"y"`
|
||||||
@@ -42,29 +49,29 @@ type Output struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mode Mode
|
Mode Mode
|
||||||
OutputName string
|
OutputName string
|
||||||
IncludeCursor bool
|
Cursor CursorMode
|
||||||
Format Format
|
Format Format
|
||||||
Quality int
|
Quality int
|
||||||
OutputDir string
|
OutputDir string
|
||||||
Filename string
|
Filename string
|
||||||
Clipboard bool
|
Clipboard bool
|
||||||
SaveFile bool
|
SaveFile bool
|
||||||
Notify bool
|
Notify bool
|
||||||
Stdout bool
|
Stdout bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Mode: ModeRegion,
|
Mode: ModeRegion,
|
||||||
IncludeCursor: false,
|
Cursor: CursorOff,
|
||||||
Format: FormatPNG,
|
Format: FormatPNG,
|
||||||
Quality: 90,
|
Quality: 90,
|
||||||
OutputDir: "",
|
OutputDir: "",
|
||||||
Filename: "",
|
Filename: "",
|
||||||
Clipboard: true,
|
Clipboard: true,
|
||||||
SaveFile: true,
|
SaveFile: true,
|
||||||
Notify: true,
|
Notify: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ func (m *Manager) Rescan() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.sysfsReady && m.sysfsBackend != nil {
|
||||||
|
if err := m.sysfsBackend.Rescan(); err != nil {
|
||||||
|
log.Debugf("Sysfs rescan failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ func shouldSuppressDevice(name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *SysfsBackend) Rescan() error {
|
||||||
|
return b.scanDevices()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *SysfsBackend) GetDevices() ([]Device, error) {
|
func (b *SysfsBackend) GetDevices() ([]Device, error) {
|
||||||
devices := make([]Device, 0)
|
devices := make([]Device, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,31 @@ func (m *Manager) initializeScreensaver() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screensaverIface := introspect.Interface{
|
||||||
|
Name: dbusScreensaverInterface,
|
||||||
|
Methods: []introspect.Method{
|
||||||
|
{
|
||||||
|
Name: "Inhibit",
|
||||||
|
Args: []introspect.Arg{
|
||||||
|
{Name: "application_name", Type: "s", Direction: "in"},
|
||||||
|
{Name: "reason_for_inhibit", Type: "s", Direction: "in"},
|
||||||
|
{Name: "cookie", Type: "u", Direction: "out"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "UnInhibit",
|
||||||
|
Args: []introspect.Arg{
|
||||||
|
{Name: "cookie", Type: "u", Direction: "in"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
introNode := &introspect.Node{
|
introNode := &introspect.Node{
|
||||||
Name: dbusScreensaverPath,
|
Name: dbusScreensaverPath,
|
||||||
Interfaces: []introspect.Interface{
|
Interfaces: []introspect.Interface{
|
||||||
introspect.IntrospectData,
|
introspect.IntrospectData,
|
||||||
{Name: dbusScreensaverInterface},
|
screensaverIface,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||||
@@ -67,7 +87,7 @@ func (m *Manager) initializeScreensaver() error {
|
|||||||
Name: dbusScreensaverPath2,
|
Name: dbusScreensaverPath2,
|
||||||
Interfaces: []introspect.Interface{
|
Interfaces: []introspect.Interface{
|
||||||
introspect.IntrospectData,
|
introspect.IntrospectData,
|
||||||
{Name: dbusScreensaverInterface},
|
screensaverIface,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ type SecretAgent struct {
|
|||||||
backend *NetworkManagerBackend
|
backend *NetworkManagerBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
type nmVariantMap map[string]dbus.Variant
|
type (
|
||||||
type nmSettingMap map[string]nmVariantMap
|
nmVariantMap map[string]dbus.Variant
|
||||||
|
nmSettingMap map[string]nmVariantMap
|
||||||
|
)
|
||||||
|
|
||||||
const introspectXML = `
|
const introspectXML = `
|
||||||
<node>
|
<node>
|
||||||
@@ -122,7 +124,7 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
|
|
||||||
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
||||||
ssid := readSSID(conn)
|
ssid := readSSID(conn)
|
||||||
fields := fieldsNeeded(settingName, hints)
|
fields := fieldsNeeded(settingName, hints, conn)
|
||||||
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
|
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
|
||||||
|
|
||||||
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
|
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
|
||||||
@@ -218,8 +220,16 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
out[settingName] = nmVariantMap{}
|
out[settingName] = nmVariantMap{}
|
||||||
return out, nil
|
return out, nil
|
||||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
||||||
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
|
switch settingName {
|
||||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
case "802-11-wireless-security":
|
||||||
|
fields = []string{"psk"}
|
||||||
|
case "802-1x":
|
||||||
|
fields = infer8021xFields(conn)
|
||||||
|
default:
|
||||||
|
log.Warnf("[SecretAgent] Agent-owned secrets for unhandled setting %s (flags=%d)", settingName, passwordFlags)
|
||||||
|
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||||
|
}
|
||||||
|
log.Infof("[SecretAgent] Agent-owned secrets, inferred fields: %v", fields)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
||||||
out := nmSettingMap{}
|
out := nmSettingMap{}
|
||||||
@@ -300,6 +310,63 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
a.backend.cachedVPNCredsMu.Unlock()
|
a.backend.cachedVPNCredsMu.Unlock()
|
||||||
|
|
||||||
|
a.backend.cachedGPSamlMu.Lock()
|
||||||
|
cachedGPSaml := a.backend.cachedGPSamlCookie
|
||||||
|
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
|
||||||
|
a.backend.cachedGPSamlMu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
|
||||||
|
|
||||||
|
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
|
||||||
|
}
|
||||||
|
a.backend.cachedGPSamlMu.Unlock()
|
||||||
|
|
||||||
|
if len(fields) == 1 && fields[0] == "gp-saml" {
|
||||||
|
gateway := ""
|
||||||
|
protocol := ""
|
||||||
|
if vpnSettings, ok := conn["vpn"]; ok {
|
||||||
|
if dataVariant, ok := vpnSettings["data"]; ok {
|
||||||
|
if dataMap, ok := dataVariant.Value().(map[string]string); ok {
|
||||||
|
if gw, ok := dataMap["gateway"]; ok {
|
||||||
|
gateway = gw
|
||||||
|
}
|
||||||
|
if proto, ok := dataMap["protocol"]; ok && proto != "" {
|
||||||
|
protocol = proto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if protocol != "gp" {
|
||||||
|
return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway)
|
||||||
|
|
||||||
|
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
defer samlCancel()
|
||||||
|
|
||||||
|
authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err)
|
||||||
|
return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
|
||||||
|
|
||||||
|
a.backend.cachedGPSamlMu.Lock()
|
||||||
|
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||||
|
ConnectionUUID: connUuid,
|
||||||
|
Cookie: authResult.Cookie,
|
||||||
|
Host: authResult.Host,
|
||||||
|
User: authResult.User,
|
||||||
|
Fingerprint: authResult.Fingerprint,
|
||||||
|
}
|
||||||
|
a.backend.cachedGPSamlMu.Unlock()
|
||||||
|
|
||||||
|
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
@@ -418,8 +485,19 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
|
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
|
||||||
}
|
}
|
||||||
case "802-1x":
|
case "802-1x":
|
||||||
out[settingName] = sec
|
secretsOnly := nmVariantMap{}
|
||||||
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
|
for k, v := range reply.Secrets {
|
||||||
|
switch k {
|
||||||
|
case "password", "private-key-password", "phase2-private-key-password", "pin":
|
||||||
|
secretsOnly[k] = dbus.MakeVariant(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[settingName] = secretsOnly
|
||||||
|
|
||||||
|
if identity, ok := reply.Secrets["identity"]; ok && identity != "" {
|
||||||
|
a.save8021xIdentity(path, identity)
|
||||||
|
}
|
||||||
|
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(secretsOnly))
|
||||||
default:
|
default:
|
||||||
out[settingName] = sec
|
out[settingName] = sec
|
||||||
}
|
}
|
||||||
@@ -434,63 +512,6 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
a.backend.pendingVPNSaveMu.Unlock()
|
a.backend.pendingVPNSaveMu.Unlock()
|
||||||
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
|
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
|
||||||
} else if reply.Save && settingName != "vpn" {
|
|
||||||
// Non-VPN save logic
|
|
||||||
go func() {
|
|
||||||
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
|
|
||||||
|
|
||||||
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
|
|
||||||
var existingSettings map[string]map[string]dbus.Variant
|
|
||||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
|
||||||
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := make(map[string]map[string]dbus.Variant)
|
|
||||||
if connSection, ok := existingSettings["connection"]; ok {
|
|
||||||
settings["connection"] = connSection
|
|
||||||
}
|
|
||||||
|
|
||||||
switch settingName {
|
|
||||||
case "802-11-wireless-security":
|
|
||||||
wifiSec, ok := existingSettings["802-11-wireless-security"]
|
|
||||||
if !ok {
|
|
||||||
wifiSec = make(map[string]dbus.Variant)
|
|
||||||
}
|
|
||||||
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
|
|
||||||
|
|
||||||
if psk, ok := reply.Secrets["psk"]; ok {
|
|
||||||
wifiSec["psk"] = dbus.MakeVariant(psk)
|
|
||||||
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
|
|
||||||
}
|
|
||||||
settings["802-11-wireless-security"] = wifiSec
|
|
||||||
|
|
||||||
case "802-1x":
|
|
||||||
dot1x, ok := existingSettings["802-1x"]
|
|
||||||
if !ok {
|
|
||||||
dot1x = make(map[string]dbus.Variant)
|
|
||||||
}
|
|
||||||
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
|
|
||||||
|
|
||||||
if password, ok := reply.Secrets["password"]; ok {
|
|
||||||
dot1x["password"] = dbus.MakeVariant(password)
|
|
||||||
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
|
|
||||||
}
|
|
||||||
settings["802-1x"] = dot1x
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Update2 with correct signature:
|
|
||||||
// Update2(IN settings, IN flags, IN args) -> OUT result
|
|
||||||
// flags: 0x1 = to-disk
|
|
||||||
var result map[string]dbus.Variant
|
|
||||||
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
|
||||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
@@ -523,6 +544,35 @@ func (a *SecretAgent) Introspect() (string, *dbus.Error) {
|
|||||||
return introspectXML, nil
|
return introspectXML, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *SecretAgent) save8021xIdentity(path dbus.ObjectPath, identity string) {
|
||||||
|
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
|
||||||
|
var existing map[string]map[string]dbus.Variant
|
||||||
|
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existing); err != nil {
|
||||||
|
log.Warnf("[SecretAgent] Failed to get settings for identity save: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := make(map[string]map[string]dbus.Variant)
|
||||||
|
if connSection, ok := existing["connection"]; ok {
|
||||||
|
settings["connection"] = connSection
|
||||||
|
}
|
||||||
|
|
||||||
|
dot1x, ok := existing["802-1x"]
|
||||||
|
if !ok {
|
||||||
|
dot1x = make(map[string]dbus.Variant)
|
||||||
|
}
|
||||||
|
dot1x["identity"] = dbus.MakeVariant(identity)
|
||||||
|
settings["802-1x"] = dot1x
|
||||||
|
|
||||||
|
var result map[string]dbus.Variant
|
||||||
|
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||||
|
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
||||||
|
log.Warnf("[SecretAgent] Failed to save 802.1x identity: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("[SecretAgent] Saved 802.1x identity to connection profile")
|
||||||
|
}
|
||||||
|
|
||||||
func readSSID(conn map[string]nmVariantMap) string {
|
func readSSID(conn map[string]nmVariantMap) string {
|
||||||
if w, ok := conn["802-11-wireless"]; ok {
|
if w, ok := conn["802-11-wireless"]; ok {
|
||||||
if v, ok := w["ssid"]; ok {
|
if v, ok := w["ssid"]; ok {
|
||||||
@@ -564,12 +614,15 @@ func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string)
|
|||||||
return connType, name, svc
|
return connType, name, svc
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldsNeeded(setting string, hints []string) []string {
|
func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string {
|
||||||
switch setting {
|
switch setting {
|
||||||
case "802-11-wireless-security":
|
case "802-11-wireless-security":
|
||||||
return []string{"psk"}
|
return []string{"psk"}
|
||||||
case "802-1x":
|
case "802-1x":
|
||||||
return []string{"identity", "password"}
|
if len(hints) > 0 {
|
||||||
|
return hints
|
||||||
|
}
|
||||||
|
return infer8021xFields(conn)
|
||||||
case "vpn":
|
case "vpn":
|
||||||
return hints
|
return hints
|
||||||
default:
|
default:
|
||||||
@@ -577,6 +630,41 @@ func fieldsNeeded(setting string, hints []string) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func infer8021xFields(conn map[string]nmVariantMap) []string {
|
||||||
|
dot1x, ok := conn["802-1x"]
|
||||||
|
if !ok {
|
||||||
|
return []string{"identity", "password"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields []string
|
||||||
|
|
||||||
|
if v, ok := dot1x["identity"]; ok {
|
||||||
|
if id, ok := v.Value().(string); ok && id != "" {
|
||||||
|
// identity already stored, don't ask again
|
||||||
|
} else {
|
||||||
|
fields = append(fields, "identity")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fields = append(fields, "identity")
|
||||||
|
}
|
||||||
|
|
||||||
|
var eapMethods []string
|
||||||
|
if v, ok := dot1x["eap"]; ok {
|
||||||
|
if methods, ok := v.Value().([]string); ok {
|
||||||
|
eapMethods = methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(eapMethods) > 0 && eapMethods[0] == "tls":
|
||||||
|
fields = append(fields, "private-key-password")
|
||||||
|
default:
|
||||||
|
fields = append(fields, "password")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
|
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
|
||||||
result := make([]FieldInfo, 0, len(fields))
|
result := make([]FieldInfo, 0, len(fields))
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
@@ -630,12 +718,25 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(vpnService, "openconnect"):
|
case strings.Contains(vpnService, "openconnect"):
|
||||||
|
protocol := dataMap["protocol"]
|
||||||
authType := dataMap["authtype"]
|
authType := dataMap["authtype"]
|
||||||
userCert := dataMap["usercert"]
|
username := dataMap["username"]
|
||||||
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
|
|
||||||
|
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
|
||||||
return []string{"key_pass"}
|
return []string{"key_pass"}
|
||||||
}
|
}
|
||||||
if dataMap["username"] == "" {
|
|
||||||
|
if needsExternalBrowserAuth(protocol, authType, username, dataMap) {
|
||||||
|
switch protocol {
|
||||||
|
case "gp":
|
||||||
|
log.Infof("[SecretAgent] GlobalProtect SAML auth detected")
|
||||||
|
return []string{"gp-saml"}
|
||||||
|
default:
|
||||||
|
log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if username == "" {
|
||||||
fields = []string{"username", "password"}
|
fields = []string{"username", "password"}
|
||||||
}
|
}
|
||||||
case strings.Contains(vpnService, "openvpn"):
|
case strings.Contains(vpnService, "openvpn"):
|
||||||
@@ -654,8 +755,31 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
|
||||||
|
if method, ok := data["saml-auth-method"]; ok {
|
||||||
|
if method == "REDIRECT" || method == "POST" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authType != "" && authType != "password" && authType != "cert" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch protocol {
|
||||||
|
case "gp":
|
||||||
|
if authType == "" && username == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
||||||
switch field {
|
switch field {
|
||||||
|
case "gp-saml":
|
||||||
|
return "GlobalProtect SAML/SSO", false
|
||||||
case "key_pass":
|
case "key_pass":
|
||||||
return "PIN", true
|
return "PIN", true
|
||||||
case "password":
|
case "password":
|
||||||
@@ -756,3 +880,18 @@ func reasonFromFlags(flags uint32) string {
|
|||||||
}
|
}
|
||||||
return "required"
|
return "required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||||
|
out := nmSettingMap{}
|
||||||
|
vpnSec := nmVariantMap{}
|
||||||
|
|
||||||
|
secrets := map[string]string{
|
||||||
|
"cookie": cookie,
|
||||||
|
"gateway": host,
|
||||||
|
"gwcert": fingerprint,
|
||||||
|
}
|
||||||
|
vpnSec["secrets"] = dbus.MakeVariant(secrets)
|
||||||
|
|
||||||
|
out[settingName] = vpnSec
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
355
core/internal/server/network/agent_networkmanager_test.go
Normal file
355
core/internal/server/network/agent_networkmanager_test.go
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNeedsExternalBrowserAuth(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
protocol string
|
||||||
|
authType string
|
||||||
|
username string
|
||||||
|
data map[string]string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GP with saml-auth-method REDIRECT",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "password",
|
||||||
|
username: "user",
|
||||||
|
data: map[string]string{"saml-auth-method": "REDIRECT"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with saml-auth-method POST",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "password",
|
||||||
|
username: "user",
|
||||||
|
data: map[string]string{"saml-auth-method": "POST"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with no authtype and no username",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with username and password authtype",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "password",
|
||||||
|
username: "john",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with username but no authtype",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "",
|
||||||
|
username: "john",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with authtype but no username - should detect SAML",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pulse with SAML",
|
||||||
|
protocol: "pulse",
|
||||||
|
authType: "",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{"saml-auth-method": "REDIRECT"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fortinet with non-password authtype",
|
||||||
|
protocol: "fortinet",
|
||||||
|
authType: "saml",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anyconnect with cert",
|
||||||
|
protocol: "anyconnect",
|
||||||
|
authType: "cert",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "anyconnect with password",
|
||||||
|
protocol: "anyconnect",
|
||||||
|
authType: "password",
|
||||||
|
username: "user",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty protocol",
|
||||||
|
protocol: "",
|
||||||
|
authType: "",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with cert authtype",
|
||||||
|
protocol: "gp",
|
||||||
|
authType: "cert",
|
||||||
|
username: "",
|
||||||
|
data: map[string]string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := needsExternalBrowserAuth(tt.protocol, tt.authType, tt.username, tt.data)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildGPSamlSecretsResponse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingName string
|
||||||
|
cookie string
|
||||||
|
host string
|
||||||
|
fingerprint string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all fields populated",
|
||||||
|
settingName: "vpn",
|
||||||
|
cookie: "authcookie=abc123&portal=GATE",
|
||||||
|
host: "vpn.example.com",
|
||||||
|
fingerprint: "pin-sha256:ABCD1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty fingerprint",
|
||||||
|
settingName: "vpn",
|
||||||
|
cookie: "authcookie=xyz",
|
||||||
|
host: "10.0.0.1",
|
||||||
|
fingerprint: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex cookie with special chars",
|
||||||
|
settingName: "vpn",
|
||||||
|
cookie: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
|
||||||
|
host: "connect.seclore.com",
|
||||||
|
fingerprint: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
|
||||||
|
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Contains(t, result, tt.settingName)
|
||||||
|
|
||||||
|
vpnSec := result[tt.settingName]
|
||||||
|
assert.NotNil(t, vpnSec)
|
||||||
|
|
||||||
|
secretsVariant, ok := vpnSec["secrets"]
|
||||||
|
assert.True(t, ok, "secrets key should exist")
|
||||||
|
|
||||||
|
secrets, ok := secretsVariant.Value().(map[string]string)
|
||||||
|
assert.True(t, ok, "secrets should be map[string]string")
|
||||||
|
|
||||||
|
assert.Equal(t, tt.cookie, secrets["cookie"])
|
||||||
|
assert.Equal(t, tt.host, secrets["gateway"])
|
||||||
|
assert.Equal(t, tt.fingerprint, secrets["gwcert"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVpnFieldMeta_GPSaml(t *testing.T) {
|
||||||
|
label, isSecret := vpnFieldMeta("gp-saml", "org.freedesktop.NetworkManager.openconnect")
|
||||||
|
|
||||||
|
assert.Equal(t, "GlobalProtect SAML/SSO", label)
|
||||||
|
assert.False(t, isSecret, "gp-saml should not be marked as secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVpnFieldMeta_StandardFields(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
field string
|
||||||
|
vpnService string
|
||||||
|
expectedLabel string
|
||||||
|
expectedSecret bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
field: "username",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
expectedLabel: "Username",
|
||||||
|
expectedSecret: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "password",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
expectedLabel: "Password",
|
||||||
|
expectedSecret: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "key_pass",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
expectedLabel: "PIN",
|
||||||
|
expectedSecret: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.field, func(t *testing.T) {
|
||||||
|
label, isSecret := vpnFieldMeta(tt.field, tt.vpnService)
|
||||||
|
assert.Equal(t, tt.expectedLabel, label)
|
||||||
|
assert.Equal(t, tt.expectedSecret, isSecret)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInferVPNFields_GPSaml(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
vpnService string
|
||||||
|
dataMap map[string]string
|
||||||
|
expectedLen int
|
||||||
|
shouldHave []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "GP with no authtype and no username - should require SAML",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"gp-saml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with saml-auth-method REDIRECT",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
"saml-auth-method": "REDIRECT",
|
||||||
|
"username": "john",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"gp-saml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with saml-auth-method POST",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
"saml-auth-method": "POST",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"gp-saml"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with username and password authtype - should use credentials",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
"authtype": "password",
|
||||||
|
"username": "john",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"password"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with username but no authtype - password only",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
"username": "john",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"password"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GP with PKCS11 cert",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "gp",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
"authtype": "cert",
|
||||||
|
"usercert": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"key_pass"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-GP protocol (anyconnect)",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openconnect",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"protocol": "anyconnect",
|
||||||
|
"gateway": "vpn.example.com",
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
shouldHave: []string{"username", "password"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OpenVPN with username",
|
||||||
|
vpnService: "org.freedesktop.NetworkManager.openvpn",
|
||||||
|
dataMap: map[string]string{
|
||||||
|
"connection-type": "password",
|
||||||
|
"username": "john",
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
shouldHave: []string{"password"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Convert dataMap to nmVariantMap
|
||||||
|
vpnSettings := make(nmVariantMap)
|
||||||
|
vpnSettings["data"] = dbus.MakeVariant(tt.dataMap)
|
||||||
|
vpnSettings["service-type"] = dbus.MakeVariant(tt.vpnService)
|
||||||
|
|
||||||
|
conn := make(map[string]nmVariantMap)
|
||||||
|
conn["vpn"] = vpnSettings
|
||||||
|
|
||||||
|
fields := inferVPNFields(conn, tt.vpnService)
|
||||||
|
|
||||||
|
assert.Len(t, fields, tt.expectedLen, "unexpected number of fields")
|
||||||
|
if len(tt.shouldHave) > 0 {
|
||||||
|
for _, expected := range tt.shouldHave {
|
||||||
|
assert.Contains(t, fields, expected, "should contain field: %s", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNmVariantMap(t *testing.T) {
|
||||||
|
// Test that nmVariantMap and nmSettingMap work correctly
|
||||||
|
settingMap := make(nmSettingMap)
|
||||||
|
variantMap := make(nmVariantMap)
|
||||||
|
|
||||||
|
variantMap["test-key"] = dbus.MakeVariant("test-value")
|
||||||
|
settingMap["test-setting"] = variantMap
|
||||||
|
|
||||||
|
assert.Contains(t, settingMap, "test-setting")
|
||||||
|
assert.Contains(t, settingMap["test-setting"], "test-key")
|
||||||
|
|
||||||
|
value := settingMap["test-setting"]["test-key"].Value()
|
||||||
|
assert.Equal(t, "test-value", value)
|
||||||
|
}
|
||||||
@@ -69,12 +69,14 @@ type NetworkManagerBackend struct {
|
|||||||
lastFailedTime int64
|
lastFailedTime int64
|
||||||
failedMutex sync.RWMutex
|
failedMutex sync.RWMutex
|
||||||
|
|
||||||
pendingVPNSave *pendingVPNCredentials
|
pendingVPNSave *pendingVPNCredentials
|
||||||
pendingVPNSaveMu sync.Mutex
|
pendingVPNSaveMu sync.Mutex
|
||||||
cachedVPNCreds *cachedVPNCredentials
|
cachedVPNCreds *cachedVPNCredentials
|
||||||
cachedVPNCredsMu sync.Mutex
|
cachedVPNCredsMu sync.Mutex
|
||||||
cachedPKCS11PIN *cachedPKCS11PIN
|
cachedPKCS11PIN *cachedPKCS11PIN
|
||||||
cachedPKCS11Mu sync.Mutex
|
cachedPKCS11Mu sync.Mutex
|
||||||
|
cachedGPSamlCookie *cachedGPSamlCookie
|
||||||
|
cachedGPSamlMu sync.Mutex
|
||||||
|
|
||||||
onStateChange func()
|
onStateChange func()
|
||||||
}
|
}
|
||||||
@@ -97,6 +99,14 @@ type cachedPKCS11PIN struct {
|
|||||||
PIN string
|
PIN string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type cachedGPSamlCookie struct {
|
||||||
|
ConnectionUUID string
|
||||||
|
Cookie string
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
Fingerprint string
|
||||||
|
}
|
||||||
|
|
||||||
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
|
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
|
||||||
var nm gonetworkmanager.NetworkManager
|
var nm gonetworkmanager.NetworkManager
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
203
core/internal/server/network/backend_networkmanager_gp_saml.go
Normal file
203
core/internal/server/network/backend_networkmanager_gp_saml.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gpSamlAuthResult struct {
|
||||||
|
Cookie string
|
||||||
|
Host string
|
||||||
|
User string
|
||||||
|
Fingerprint string
|
||||||
|
}
|
||||||
|
|
||||||
|
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
|
||||||
|
// Only supports protocol=gp. Other protocols need their own implementations.
|
||||||
|
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
|
||||||
|
if gateway == "" {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
|
||||||
|
}
|
||||||
|
if protocol != "gp" {
|
||||||
|
return nil, fmt.Errorf("only GlobalProtect (protocol=gp) SAML is supported, got: %s", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[GP-SAML] Starting GlobalProtect SAML authentication with gp-saml-gui for gateway=%s", gateway)
|
||||||
|
|
||||||
|
gpSamlPath, err := exec.LookPath("gp-saml-gui")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GlobalProtect SAML requires gp-saml-gui (install: pip install gp-saml-gui): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--gateway",
|
||||||
|
"--allow-insecure-crypto",
|
||||||
|
gateway,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, gpSamlPath, args...)
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: failed to create stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: failed to create stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: failed to start gp-saml-gui: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
scanner := bufio.NewScanner(stderr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
log.Debugf("[GP-SAML] gp-saml-gui: %s", scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
result := &gpSamlAuthResult{Host: gateway}
|
||||||
|
var allOutput []string
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(stdout)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
allOutput = append(allOutput, line)
|
||||||
|
log.Infof("[GP-SAML] stdout: %s", line)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "COOKIE="):
|
||||||
|
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
|
||||||
|
case strings.HasPrefix(line, "HOST="):
|
||||||
|
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
|
||||||
|
case strings.HasPrefix(line, "USER="):
|
||||||
|
result.User = unshellQuote(strings.TrimPrefix(line, "USER="))
|
||||||
|
case strings.HasPrefix(line, "FINGERPRINT="):
|
||||||
|
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
|
||||||
|
default:
|
||||||
|
parseGPSamlFromCommandLine(line, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth timed out or was cancelled: %w", ctx.Err())
|
||||||
|
}
|
||||||
|
if result.Cookie == "" {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth failed: %w (output: %s)", err, strings.Join(allOutput, "\n"))
|
||||||
|
}
|
||||||
|
log.Warnf("[GP-SAML] gp-saml-gui exited with error but cookie was captured: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Cookie == "" {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: no cookie in gp-saml-gui output")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[GP-SAML] Got prelogin-cookie from gp-saml-gui, converting to openconnect cookie via --authenticate")
|
||||||
|
|
||||||
|
// Convert prelogin-cookie to full openconnect cookie format
|
||||||
|
ocResult, err := convertGPPreloginCookie(ctx, gateway, result.Cookie, result.User)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GP SAML auth: failed to convert prelogin-cookie: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Cookie = ocResult.Cookie
|
||||||
|
result.Host = ocResult.Host
|
||||||
|
result.Fingerprint = ocResult.Fingerprint
|
||||||
|
|
||||||
|
log.Infof("[GP-SAML] Authentication successful: user=%s, host=%s, cookie_len=%d, has_fingerprint=%v",
|
||||||
|
result.User, result.Host, len(result.Cookie), result.Fingerprint != "")
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
|
||||||
|
ocPath, err := exec.LookPath("openconnect")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("openconnect not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--protocol=gp",
|
||||||
|
"--usergroup=gateway:prelogin-cookie",
|
||||||
|
"--user=" + user,
|
||||||
|
"--passwd-on-stdin",
|
||||||
|
"--allow-insecure-crypto",
|
||||||
|
"--authenticate",
|
||||||
|
gateway,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, ocPath, args...)
|
||||||
|
cmd.Stdin = strings.NewReader(preloginCookie)
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &gpSamlAuthResult{}
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "COOKIE="):
|
||||||
|
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
|
||||||
|
case strings.HasPrefix(line, "HOST="):
|
||||||
|
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
|
||||||
|
case strings.HasPrefix(line, "FINGERPRINT="):
|
||||||
|
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
|
||||||
|
case strings.HasPrefix(line, "CONNECT_URL="):
|
||||||
|
connectURL := unshellQuote(strings.TrimPrefix(line, "CONNECT_URL="))
|
||||||
|
if connectURL != "" && result.Host == "" {
|
||||||
|
result.Host = connectURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Cookie == "" {
|
||||||
|
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
|
||||||
|
len(result.Cookie), result.Host, result.Fingerprint)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unshellQuote(s string) string {
|
||||||
|
if len(s) >= 2 {
|
||||||
|
if (s[0] == '\'' && s[len(s)-1] == '\'') ||
|
||||||
|
(s[0] == '"' && s[len(s)-1] == '"') {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
|
||||||
|
if !strings.Contains(line, "openconnect") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range strings.Fields(line) {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(part, "--cookie="):
|
||||||
|
if result.Cookie == "" {
|
||||||
|
result.Cookie = strings.TrimPrefix(part, "--cookie=")
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(part, "--servercert="):
|
||||||
|
if result.Fingerprint == "" {
|
||||||
|
result.Fingerprint = strings.TrimPrefix(part, "--servercert=")
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(part, "--user="):
|
||||||
|
if result.User == "" {
|
||||||
|
result.User = strings.TrimPrefix(part, "--user=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnshellQuote(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single quoted",
|
||||||
|
input: "'hello world'",
|
||||||
|
expected: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "double quoted",
|
||||||
|
input: `"hello world"`,
|
||||||
|
expected: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unquoted",
|
||||||
|
input: "hello",
|
||||||
|
expected: "hello",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty single quotes",
|
||||||
|
input: "''",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty double quotes",
|
||||||
|
input: `""`,
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single quote only",
|
||||||
|
input: "'",
|
||||||
|
expected: "'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatched quotes",
|
||||||
|
input: "'hello\"",
|
||||||
|
expected: "'hello\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with special chars",
|
||||||
|
input: "'cookie=abc123&user=john'",
|
||||||
|
expected: "cookie=abc123&user=john",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex cookie",
|
||||||
|
input: `'authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100'`,
|
||||||
|
expected: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := unshellQuote(tt.input)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseGPSamlFromCommandLine(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
line string
|
||||||
|
initialResult *gpSamlAuthResult
|
||||||
|
expectedCookie string
|
||||||
|
expectedUser string
|
||||||
|
expectedFP string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full openconnect command",
|
||||||
|
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
|
||||||
|
initialResult: &gpSamlAuthResult{},
|
||||||
|
expectedCookie: "AUTH123",
|
||||||
|
expectedUser: "john",
|
||||||
|
expectedFP: "pin-sha256:ABC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with equals signs in cookie",
|
||||||
|
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
|
||||||
|
initialResult: &gpSamlAuthResult{},
|
||||||
|
expectedCookie: "authcookie=xyz123&portal=GATE",
|
||||||
|
expectedUser: "jane",
|
||||||
|
expectedFP: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-openconnect line",
|
||||||
|
line: "some other output",
|
||||||
|
initialResult: &gpSamlAuthResult{},
|
||||||
|
expectedCookie: "",
|
||||||
|
expectedUser: "",
|
||||||
|
expectedFP: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preserves existing values",
|
||||||
|
line: "openconnect --user=newuser",
|
||||||
|
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
|
||||||
|
expectedCookie: "existing",
|
||||||
|
expectedUser: "newuser",
|
||||||
|
expectedFP: "existing-fp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only updates empty fields",
|
||||||
|
line: "openconnect --cookie=NEW --user=NEW",
|
||||||
|
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
|
||||||
|
expectedCookie: "OLD",
|
||||||
|
expectedUser: "NEW",
|
||||||
|
expectedFP: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "real gp-saml-gui output",
|
||||||
|
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
|
||||||
|
initialResult: &gpSamlAuthResult{},
|
||||||
|
expectedCookie: "",
|
||||||
|
expectedUser: "john.doe@example.com",
|
||||||
|
expectedFP: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with server cert flag",
|
||||||
|
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
|
||||||
|
initialResult: &gpSamlAuthResult{},
|
||||||
|
expectedCookie: "",
|
||||||
|
expectedUser: "",
|
||||||
|
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.initialResult
|
||||||
|
parseGPSamlFromCommandLine(tt.line, result)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedCookie, result.Cookie, "cookie mismatch")
|
||||||
|
assert.Equal(t, tt.expectedUser, result.User, "user mismatch")
|
||||||
|
assert.Equal(t, tt.expectedFP, result.Fingerprint, "fingerprint mismatch")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
|
||||||
|
// Simulate gp-saml-gui output with command line suggestion
|
||||||
|
lines := []string{
|
||||||
|
"",
|
||||||
|
"SAML REDIRECT",
|
||||||
|
"Got SAML Login URL",
|
||||||
|
"POST to ACS endpoint...",
|
||||||
|
"Got 'prelogin-cookie': 'FAKE_cookie_12345'",
|
||||||
|
"openconnect --protocol=gp --user=john.doe@example.com --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.example.com",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &gpSamlAuthResult{}
|
||||||
|
for _, line := range lines {
|
||||||
|
parseGPSamlFromCommandLine(line, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "john.doe@example.com", result.User)
|
||||||
|
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
|
||||||
|
assert.Empty(t, result.Fingerprint)
|
||||||
|
}
|
||||||
@@ -212,32 +212,28 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var forgetSSID string
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
defer b.stateMutex.Unlock()
|
|
||||||
|
|
||||||
wasConnecting = b.state.IsConnecting
|
wasConnecting = b.state.IsConnecting
|
||||||
connectingSSID = b.state.ConnectingSSID
|
connectingSSID = b.state.ConnectingSSID
|
||||||
|
|
||||||
if wasConnecting && connectingSSID != "" {
|
if wasConnecting && connectingSSID != "" {
|
||||||
if connected && ssid == connectingSSID {
|
switch {
|
||||||
|
case connected && ssid == connectingSSID:
|
||||||
log.Infof("[updateWiFiState] Connection successful: %s", ssid)
|
log.Infof("[updateWiFiState] Connection successful: %s", ssid)
|
||||||
b.state.IsConnecting = false
|
b.state.IsConnecting = false
|
||||||
b.state.ConnectingSSID = ""
|
b.state.ConnectingSSID = ""
|
||||||
b.state.LastError = ""
|
b.state.LastError = ""
|
||||||
} else if failed || (disconnected && !connected) {
|
case failed || (disconnected && !connected):
|
||||||
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
|
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
|
||||||
b.state.IsConnecting = false
|
b.state.IsConnecting = false
|
||||||
b.state.ConnectingSSID = ""
|
b.state.ConnectingSSID = ""
|
||||||
b.state.LastError = reasonCode
|
b.state.LastError = reasonCode
|
||||||
|
|
||||||
// If user cancelled, delete the connection profile that was just created
|
|
||||||
if reasonCode == errdefs.ErrUserCanceled {
|
if reasonCode == errdefs.ErrUserCanceled {
|
||||||
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", connectingSSID)
|
forgetSSID = connectingSSID
|
||||||
b.stateMutex.Unlock()
|
|
||||||
if err := b.ForgetWiFiNetwork(connectingSSID); err != nil {
|
|
||||||
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
|
|
||||||
}
|
|
||||||
b.stateMutex.Lock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.failedMutex.Lock()
|
b.failedMutex.Lock()
|
||||||
@@ -254,6 +250,15 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
b.state.WiFiBSSID = bssid
|
b.state.WiFiBSSID = bssid
|
||||||
b.state.WiFiSignal = signal
|
b.state.WiFiSignal = signal
|
||||||
|
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if forgetSSID != "" {
|
||||||
|
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
|
||||||
|
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
|
||||||
|
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -304,6 +304,51 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
|
|||||||
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
|
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "gp_saml":
|
||||||
|
gateway := vpnData["gateway"]
|
||||||
|
protocol := vpnData["protocol"]
|
||||||
|
if protocol != "gp" {
|
||||||
|
return fmt.Errorf("GlobalProtect SAML authentication only supported for protocol=gp, got: %s", protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
|
||||||
|
|
||||||
|
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
|
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||||
|
samlCancel()
|
||||||
|
if err != nil {
|
||||||
|
errMsg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(errMsg, "not installed"):
|
||||||
|
return fmt.Errorf("gp-saml-gui is not installed (required for GlobalProtect SAML/SSO VPN)")
|
||||||
|
case strings.Contains(errMsg, "timed out") || strings.Contains(errMsg, "cancelled"):
|
||||||
|
return fmt.Errorf("GlobalProtect SAML authentication timed out — please try again")
|
||||||
|
case strings.Contains(errMsg, "no cookie"):
|
||||||
|
return fmt.Errorf("GlobalProtect SAML login did not complete — browser was closed before authentication finished")
|
||||||
|
case strings.Contains(errMsg, "convert prelogin-cookie"):
|
||||||
|
return fmt.Errorf("GlobalProtect VPN authentication succeeded but cookie exchange failed: %w", err)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("GlobalProtect SAML authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cachedGPSamlMu.Lock()
|
||||||
|
b.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||||
|
ConnectionUUID: targetUUID,
|
||||||
|
Cookie: authResult.Cookie,
|
||||||
|
Host: authResult.Host,
|
||||||
|
User: authResult.User,
|
||||||
|
Fingerprint: authResult.Fingerprint,
|
||||||
|
}
|
||||||
|
b.cachedGPSamlMu.Unlock()
|
||||||
|
|
||||||
|
if err := targetConn.ClearSecrets(); err != nil {
|
||||||
|
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
|
||||||
|
} else {
|
||||||
|
log.Infof("[ConnectVPN] Cleared stale stored secrets for %s", connName)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
@@ -339,6 +384,16 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case strings.Contains(serviceType, "openconnect"):
|
||||||
|
protocol := data["protocol"]
|
||||||
|
if needsExternalBrowserAuth(protocol, data["authtype"], data["username"], data) {
|
||||||
|
switch protocol {
|
||||||
|
case "gp":
|
||||||
|
return "gp_saml"
|
||||||
|
default:
|
||||||
|
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
case strings.Contains(serviceType, "openvpn"):
|
case strings.Contains(serviceType, "openvpn"):
|
||||||
connType := data["connection-type"]
|
connType := data["connection-type"]
|
||||||
username := data["username"]
|
username := data["username"]
|
||||||
@@ -412,16 +467,6 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
|
|||||||
}
|
}
|
||||||
data["username"] = username
|
data["username"] = username
|
||||||
|
|
||||||
if reply.Save && password != "" {
|
|
||||||
data["password-flags"] = "0"
|
|
||||||
secs := make(map[string]string)
|
|
||||||
secs["password"] = password
|
|
||||||
vpn["secrets"] = dbus.MakeVariant(secs)
|
|
||||||
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
|
|
||||||
} else {
|
|
||||||
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
|
|
||||||
}
|
|
||||||
|
|
||||||
vpn["data"] = dbus.MakeVariant(data)
|
vpn["data"] = dbus.MakeVariant(data)
|
||||||
settings["vpn"] = vpn
|
settings["vpn"] = vpn
|
||||||
|
|
||||||
@@ -432,7 +477,7 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
|
|||||||
}
|
}
|
||||||
log.Infof("[ConnectVPN] Username saved to connection")
|
log.Infof("[ConnectVPN] Username saved to connection")
|
||||||
|
|
||||||
if password != "" && !reply.Save {
|
if password != "" {
|
||||||
b.cachedVPNCredsMu.Lock()
|
b.cachedVPNCredsMu.Lock()
|
||||||
b.cachedVPNCreds = &cachedVPNCredentials{
|
b.cachedVPNCreds = &cachedVPNCredentials{
|
||||||
ConnectionUUID: targetUUID,
|
ConnectionUUID: targetUUID,
|
||||||
@@ -614,11 +659,7 @@ func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
|
|||||||
dataMap["password-flags"] = "1"
|
dataMap["password-flags"] = "1"
|
||||||
vpnSettings["data"] = dataMap
|
vpnSettings["data"] = dataMap
|
||||||
}
|
}
|
||||||
|
|
||||||
vpnSettings["password-flags"] = uint32(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settings["vpn-secrets"] = make(map[string]any)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conn.Update(settings); err != nil {
|
if err := conn.Update(settings); err != nil {
|
||||||
@@ -684,10 +725,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.LastError = ""
|
b.state.LastError = ""
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN on success
|
// Clear cached PKCS11 PIN and SAML cookie on success
|
||||||
b.cachedPKCS11Mu.Lock()
|
b.cachedPKCS11Mu.Lock()
|
||||||
b.cachedPKCS11PIN = nil
|
b.cachedPKCS11PIN = nil
|
||||||
b.cachedPKCS11Mu.Unlock()
|
b.cachedPKCS11Mu.Unlock()
|
||||||
|
b.cachedGPSamlMu.Lock()
|
||||||
|
b.cachedGPSamlCookie = nil
|
||||||
|
b.cachedGPSamlMu.Unlock()
|
||||||
|
|
||||||
b.pendingVPNSaveMu.Lock()
|
b.pendingVPNSaveMu.Lock()
|
||||||
pending := b.pendingVPNSave
|
pending := b.pendingVPNSave
|
||||||
@@ -706,10 +750,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN on failure
|
// Clear cached PKCS11 PIN and SAML cookie on failure
|
||||||
b.cachedPKCS11Mu.Lock()
|
b.cachedPKCS11Mu.Lock()
|
||||||
b.cachedPKCS11PIN = nil
|
b.cachedPKCS11PIN = nil
|
||||||
b.cachedPKCS11Mu.Unlock()
|
b.cachedPKCS11Mu.Unlock()
|
||||||
|
b.cachedGPSamlMu.Lock()
|
||||||
|
b.cachedGPSamlCookie = nil
|
||||||
|
b.cachedGPSamlMu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,10 +770,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN
|
// Clear cached PKCS11 PIN and SAML cookie
|
||||||
b.cachedPKCS11Mu.Lock()
|
b.cachedPKCS11Mu.Lock()
|
||||||
b.cachedPKCS11PIN = nil
|
b.cachedPKCS11PIN = nil
|
||||||
b.cachedPKCS11Mu.Unlock()
|
b.cachedPKCS11Mu.Unlock()
|
||||||
|
b.cachedGPSamlMu.Lock()
|
||||||
|
b.cachedGPSamlCookie = nil
|
||||||
|
b.cachedGPSamlMu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,21 +92,13 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registry, err := themes.NewRegistry()
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
allThemes, err := registry.List()
|
|
||||||
if err != nil {
|
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
themeMap := make(map[string]themes.Theme)
|
themeMap := make(map[string]themes.Theme)
|
||||||
for _, t := range allThemes {
|
if registry, err := themes.NewRegistry(); err == nil {
|
||||||
themeMap[t.ID] = t
|
if allThemes, err := registry.List(); err == nil {
|
||||||
|
for _, t := range allThemes {
|
||||||
|
themeMap[t.ID] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]ThemeInfo, 0, len(installedIDs))
|
result := make([]ThemeInfo, 0, len(installedIDs))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,3 +20,18 @@ func IsDBusServiceAvailable(busName string) bool {
|
|||||||
}
|
}
|
||||||
return owned
|
return owned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDBusServiceActivatable(busName string) bool {
|
||||||
|
conn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||||
|
var activatable []string
|
||||||
|
if err := obj.Call("org.freedesktop.DBus.ListActivatableNames", 0).Store(&activatable); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return slices.Contains(activatable, busName)
|
||||||
|
}
|
||||||
|
|||||||
37
core/internal/utils/group.go
Normal file
37
core/internal/utils/group.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HasGroup(groupName string) bool {
|
||||||
|
return HasGroupIn(groupName, "/etc/group")
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasGroupIn(groupName, path string) bool {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return HasGroupData(groupName, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasGroupData(groupName, data string) bool {
|
||||||
|
prefix := groupName + ":"
|
||||||
|
for line := range strings.SplitSeq(data, "\n") {
|
||||||
|
if strings.HasPrefix(line, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindGroupData(data string, candidates ...string) (string, bool) {
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if HasGroupData(candidate, data) {
|
||||||
|
return candidate, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
142
core/internal/utils/group_test.go
Normal file
142
core/internal/utils/group_test.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
const testGroupData = `root:x:0:brltty,root
|
||||||
|
sys:x:3:bin,testuser
|
||||||
|
mem:x:8:
|
||||||
|
ftp:x:11:
|
||||||
|
mail:x:12:
|
||||||
|
log:x:19:
|
||||||
|
smmsp:x:25:
|
||||||
|
proc:x:26:
|
||||||
|
games:x:50:
|
||||||
|
lock:x:54:
|
||||||
|
network:x:90:
|
||||||
|
floppy:x:94:
|
||||||
|
scanner:x:96:
|
||||||
|
power:x:98:
|
||||||
|
nobody:x:65534:
|
||||||
|
adm:x:999:daemon
|
||||||
|
wheel:x:998:testuser
|
||||||
|
utmp:x:997:
|
||||||
|
audio:x:996:brltty
|
||||||
|
disk:x:995:
|
||||||
|
input:x:994:brltty,testuser,greeter
|
||||||
|
kmem:x:993:
|
||||||
|
kvm:x:992:libvirt-qemu,qemu,testuser
|
||||||
|
lp:x:991:cups,testuser
|
||||||
|
optical:x:990:
|
||||||
|
render:x:989:
|
||||||
|
sgx:x:988:
|
||||||
|
storage:x:987:
|
||||||
|
tty:x:5:brltty
|
||||||
|
uucp:x:986:brltty
|
||||||
|
video:x:985:cosmic-greeter,greeter,testuser
|
||||||
|
users:x:984:
|
||||||
|
groups:x:983:
|
||||||
|
systemd-journal:x:982:
|
||||||
|
rfkill:x:981:
|
||||||
|
bin:x:1:daemon
|
||||||
|
daemon:x:2:bin
|
||||||
|
http:x:33:
|
||||||
|
dbus:x:81:
|
||||||
|
systemd-coredump:x:980:
|
||||||
|
systemd-network:x:979:
|
||||||
|
systemd-oom:x:978:
|
||||||
|
systemd-journal-remote:x:977:
|
||||||
|
systemd-resolve:x:976:
|
||||||
|
systemd-timesync:x:975:
|
||||||
|
tss:x:974:
|
||||||
|
uuidd:x:973:
|
||||||
|
alpm:x:972:
|
||||||
|
polkitd:x:102:
|
||||||
|
testuser:x:1000:
|
||||||
|
avahi:x:971:
|
||||||
|
git:x:970:
|
||||||
|
nvidia-persistenced:x:143:
|
||||||
|
i2c:x:969:testuser
|
||||||
|
seat:x:968:
|
||||||
|
rtkit:x:133:
|
||||||
|
brlapi:x:967:brltty
|
||||||
|
gdm:x:120:
|
||||||
|
brltty:x:966:
|
||||||
|
colord:x:965:
|
||||||
|
flatpak:x:964:
|
||||||
|
geoclue:x:963:testuser
|
||||||
|
gnome-remote-desktop:x:962:
|
||||||
|
saned:x:961:
|
||||||
|
usbmux:x:140:
|
||||||
|
cosmic-greeter:x:960:
|
||||||
|
greeter:x:959:testuser
|
||||||
|
openvpn:x:958:
|
||||||
|
nm-openvpn:x:957:
|
||||||
|
named:x:40:
|
||||||
|
_talkd:x:956:
|
||||||
|
keyd:x:955:
|
||||||
|
cups:x:209:testuser
|
||||||
|
docker:x:954:testuser
|
||||||
|
mysql:x:953:
|
||||||
|
radicale:x:952:
|
||||||
|
onepassword:x:1001:
|
||||||
|
nixbld:x:951:nixbld01,nixbld02,nixbld03,nixbld04,nixbld05,nixbld06,nixbld07,nixbld08,nixbld09,nixbld10
|
||||||
|
virtlogin:x:940:
|
||||||
|
libvirt:x:939:testuser
|
||||||
|
libvirt-qemu:x:938:
|
||||||
|
qemu:x:937:
|
||||||
|
dnsmasq:x:936:
|
||||||
|
clock:x:935:
|
||||||
|
dms-greeter:x:1002:greeter,testuser
|
||||||
|
pcscd:x:934:
|
||||||
|
test:x:1003:
|
||||||
|
empower:x:933:
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestHasGroupData(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
group string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"greeter", true},
|
||||||
|
{"root", true},
|
||||||
|
{"docker", true},
|
||||||
|
{"cosmic-greeter", true},
|
||||||
|
{"dms-greeter", true},
|
||||||
|
{"nonexistent", false},
|
||||||
|
{"greet", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := HasGroupData(tt.group, testGroupData); got != tt.want {
|
||||||
|
t.Errorf("HasGroupData(%q) = %v, want %v", tt.group, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindGroupData(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
candidates []string
|
||||||
|
wantGroup string
|
||||||
|
wantFound bool
|
||||||
|
}{
|
||||||
|
{"first match wins", []string{"greeter", "greetd", "_greeter"}, "greeter", true},
|
||||||
|
{"fallback to second", []string{"greetd", "greeter"}, "greeter", true},
|
||||||
|
{"none found", []string{"_greetd", "greetd"}, "", false},
|
||||||
|
{"single match", []string{"docker"}, "docker", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got, found := FindGroupData(testGroupData, tt.candidates...)
|
||||||
|
if got != tt.wantGroup || found != tt.wantFound {
|
||||||
|
t.Errorf("%s: FindGroupData(%v) = (%q, %v), want (%q, %v)",
|
||||||
|
tt.name, tt.candidates, got, found, tt.wantGroup, tt.wantFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasGroupDataEmpty(t *testing.T) {
|
||||||
|
if HasGroupData("greeter", "") {
|
||||||
|
t.Error("expected false for empty data")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,22 @@ func XDGConfigHome() string {
|
|||||||
return filepath.Join(home, ".config")
|
return filepath.Join(home, ".config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EmacsConfigDir() string {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
|
||||||
|
emacsD := filepath.Join(home, ".emacs.d")
|
||||||
|
if info, err := os.Stat(emacsD); err == nil && info.IsDir() {
|
||||||
|
return emacsD
|
||||||
|
}
|
||||||
|
|
||||||
|
xdgEmacs := filepath.Join(XDGConfigHome(), "emacs")
|
||||||
|
if info, err := os.Stat(xdgEmacs); err == nil && info.IsDir() {
|
||||||
|
return xdgEmacs
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func ExpandPath(path string) (string, error) {
|
func ExpandPath(path string) (string, error) {
|
||||||
expanded := os.ExpandEnv(path)
|
expanded := os.ExpandEnv(path)
|
||||||
expanded = filepath.Clean(expanded)
|
expanded = filepath.Clean(expanded)
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ in
|
|||||||
"hyprland"
|
"hyprland"
|
||||||
"sway"
|
"sway"
|
||||||
"labwc"
|
"labwc"
|
||||||
|
"mango"
|
||||||
|
"scroll"
|
||||||
|
"miracle"
|
||||||
];
|
];
|
||||||
description = "Compositor to run greeter in";
|
description = "Compositor to run greeter in";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,5 +50,6 @@ in
|
|||||||
|
|
||||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||||
services.accounts-daemon.enable = lib.mkDefault true;
|
services.accounts-daemon.enable = lib.mkDefault true;
|
||||||
|
security.polkit.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
sonnet
|
sonnet
|
||||||
qtmultimedia
|
qtmultimedia
|
||||||
qtimageformats
|
qtimageformats
|
||||||
|
kimageformats
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
pname = "dms-shell";
|
pname = "dms-shell";
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-vsfCgpilOHzJbTaJjJfMK/cSvtyFYJsPDjY4m3iuoFg=";
|
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ Singleton {
|
|||||||
|
|
||||||
property var currentOSDsByScreen: ({})
|
property var currentOSDsByScreen: ({})
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
const activeNames = {};
|
||||||
|
for (let i = 0; i < Quickshell.screens.length; i++)
|
||||||
|
activeNames[Quickshell.screens[i].name] = true;
|
||||||
|
for (const screenName in osdManager.currentOSDsByScreen) {
|
||||||
|
if (activeNames[screenName])
|
||||||
|
continue;
|
||||||
|
osdManager.currentOSDsByScreen[screenName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showOSD(osd) {
|
function showOSD(osd) {
|
||||||
if (!osd || !osd.screen)
|
if (!osd || !osd.screen)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ Singleton {
|
|||||||
return Quickshell.iconPath(moddedId, true);
|
return Quickshell.iconPath(moddedId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : "";
|
if (desktopEntry && desktopEntry.icon) {
|
||||||
|
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||||
|
}
|
||||||
|
return Quickshell.iconPath(appId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAppName(appId: string, desktopEntry: var): string {
|
function getAppName(appId: string, desktopEntry: var): string {
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ Singleton {
|
|||||||
property string vpnLastConnected: ""
|
property string vpnLastConnected: ""
|
||||||
|
|
||||||
property var deviceMaxVolumes: ({})
|
property var deviceMaxVolumes: ({})
|
||||||
|
property var hiddenOutputDeviceNames: []
|
||||||
|
property var hiddenInputDeviceNames: []
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isGreeterMode) {
|
if (!isGreeterMode) {
|
||||||
@@ -1069,6 +1071,20 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHiddenOutputDeviceNames(deviceNames) {
|
||||||
|
if (!Array.isArray(deviceNames))
|
||||||
|
return;
|
||||||
|
hiddenOutputDeviceNames = deviceNames;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHiddenInputDeviceNames(deviceNames) {
|
||||||
|
if (!Array.isArray(deviceNames))
|
||||||
|
return;
|
||||||
|
hiddenInputDeviceNames = deviceNames;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function getDeviceMaxVolume(nodeName) {
|
function getDeviceMaxVolume(nodeName) {
|
||||||
if (!nodeName)
|
if (!nodeName)
|
||||||
return 100;
|
return 100;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ Singleton {
|
|||||||
property bool _hasLoaded: false
|
property bool _hasLoaded: false
|
||||||
property bool _isReadOnly: false
|
property bool _isReadOnly: false
|
||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
|
property bool _selfWrite: false
|
||||||
property var _loadedSettingsSnapshot: null
|
property var _loadedSettingsSnapshot: null
|
||||||
property var pluginSettings: ({})
|
property var pluginSettings: ({})
|
||||||
property var builtInPluginSettings: ({})
|
property var builtInPluginSettings: ({})
|
||||||
@@ -79,6 +80,8 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool clipboardEnterToPaste: false
|
||||||
|
|
||||||
property var launcherPluginVisibility: ({})
|
property var launcherPluginVisibility: ({})
|
||||||
|
|
||||||
function getPluginAllowWithoutTrigger(pluginId) {
|
function getPluginAllowWithoutTrigger(pluginId) {
|
||||||
@@ -312,6 +315,7 @@ Singleton {
|
|||||||
property int dankLauncherV2BorderThickness: 2
|
property int dankLauncherV2BorderThickness: 2
|
||||||
property string dankLauncherV2BorderColor: "primary"
|
property string dankLauncherV2BorderColor: "primary"
|
||||||
property bool dankLauncherV2ShowFooter: true
|
property bool dankLauncherV2ShowFooter: true
|
||||||
|
property bool dankLauncherV2UnloadOnClose: false
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
@@ -469,6 +473,8 @@ Singleton {
|
|||||||
property bool dockShowOverflowBadge: true
|
property bool dockShowOverflowBadge: true
|
||||||
|
|
||||||
property bool notificationOverlayEnabled: false
|
property bool notificationOverlayEnabled: false
|
||||||
|
property bool notificationPopupShadowEnabled: true
|
||||||
|
property bool notificationPopupPrivacyMode: false
|
||||||
property int overviewRows: 2
|
property int overviewRows: 2
|
||||||
property int overviewColumns: 5
|
property int overviewColumns: 5
|
||||||
property real overviewScale: 0.16
|
property real overviewScale: 0.16
|
||||||
@@ -498,12 +504,15 @@ Singleton {
|
|||||||
property int notificationTimeoutCritical: 0
|
property int notificationTimeoutCritical: 0
|
||||||
property bool notificationCompactMode: false
|
property bool notificationCompactMode: false
|
||||||
property int notificationPopupPosition: SettingsData.Position.Top
|
property int notificationPopupPosition: SettingsData.Position.Top
|
||||||
|
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||||
|
property int notificationCustomAnimationDuration: 400
|
||||||
property bool notificationHistoryEnabled: true
|
property bool notificationHistoryEnabled: true
|
||||||
property int notificationHistoryMaxCount: 50
|
property int notificationHistoryMaxCount: 50
|
||||||
property int notificationHistoryMaxAgeDays: 7
|
property int notificationHistoryMaxAgeDays: 7
|
||||||
property bool notificationHistorySaveLow: true
|
property bool notificationHistorySaveLow: true
|
||||||
property bool notificationHistorySaveNormal: true
|
property bool notificationHistorySaveNormal: true
|
||||||
property bool notificationHistorySaveCritical: true
|
property bool notificationHistorySaveCritical: true
|
||||||
|
property var notificationRules: []
|
||||||
|
|
||||||
property bool osdAlwaysShowValue: false
|
property bool osdAlwaysShowValue: false
|
||||||
property int osdPosition: SettingsData.Position.BottomCenter
|
property int osdPosition: SettingsData.Position.BottomCenter
|
||||||
@@ -1006,6 +1015,42 @@ Singleton {
|
|||||||
function applyStoredIconTheme() {
|
function applyStoredIconTheme() {
|
||||||
updateGtkIconTheme();
|
updateGtkIconTheme();
|
||||||
updateQtIconTheme();
|
updateQtIconTheme();
|
||||||
|
updateCosmicIconTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCosmicIconTheme() {
|
||||||
|
let cosmicThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme;
|
||||||
|
if (!cosmicThemeName || cosmicThemeName === "System Default") {
|
||||||
|
const detectScript = `if command -v gsettings >/dev/null 2>&1; then
|
||||||
|
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
|
||||||
|
elif command -v dconf >/dev/null 2>&1; then
|
||||||
|
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
|
||||||
|
fi`;
|
||||||
|
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
|
||||||
|
if (exitCode !== 0)
|
||||||
|
return;
|
||||||
|
const detected = (output || "").trim();
|
||||||
|
if (!detected || detected === "System Default")
|
||||||
|
return;
|
||||||
|
const detectedEscaped = detected.replace(/'/g, "'\\''");
|
||||||
|
const writeScript = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
|
||||||
|
printf '"%s"\\n' '${detectedEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
|
||||||
|
Quickshell.execDetached(["sh", "-lc", writeScript]);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cosmicThemeNameEscaped = cosmicThemeName.replace(/'/g, "'\\''");
|
||||||
|
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
|
||||||
|
printf '"%s"\\n' '${cosmicThemeNameEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
|
||||||
|
Quickshell.execDetached(["sh", "-lc", script]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCosmicThemeMode(isLightMode) {
|
||||||
|
const isDark = isLightMode ? "false" : "true";
|
||||||
|
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1
|
||||||
|
printf '%s\\n' ${isDark} > ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark 2>/dev/null || true`;
|
||||||
|
Quickshell.execDetached(["sh", "-lc", script]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGtkIconTheme() {
|
function updateGtkIconTheme() {
|
||||||
@@ -1199,6 +1244,7 @@ Singleton {
|
|||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (_loading || _parseError || !_hasLoaded)
|
if (_loading || _parseError || !_hasLoaded)
|
||||||
return;
|
return;
|
||||||
|
_selfWrite = true;
|
||||||
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
|
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
_checkSettingsWritable();
|
_checkSettingsWritable();
|
||||||
@@ -1812,6 +1858,7 @@ Singleton {
|
|||||||
iconTheme = themeName;
|
iconTheme = themeName;
|
||||||
updateGtkIconTheme();
|
updateGtkIconTheme();
|
||||||
updateQtIconTheme();
|
updateQtIconTheme();
|
||||||
|
updateCosmicIconTheme();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
|
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
|
||||||
Theme.generateSystemThemesFromCurrentTheme();
|
Theme.generateSystemThemesFromCurrentTheme();
|
||||||
@@ -2134,6 +2181,143 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool _pendingExpandNotificationRules: false
|
||||||
|
property int _pendingNotificationRuleIndex: -1
|
||||||
|
|
||||||
|
function addNotificationRule() {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
rules.push({
|
||||||
|
enabled: true,
|
||||||
|
field: "appName",
|
||||||
|
pattern: "",
|
||||||
|
matchType: "contains",
|
||||||
|
action: "default",
|
||||||
|
urgency: "default"
|
||||||
|
});
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNotificationRuleForNotification(appName, desktopEntry) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
|
||||||
|
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
|
||||||
|
var rule = {
|
||||||
|
enabled: true,
|
||||||
|
field: pattern ? field : "appName",
|
||||||
|
pattern: pattern || "",
|
||||||
|
matchType: pattern ? "exact" : "contains",
|
||||||
|
action: "default",
|
||||||
|
urgency: "default"
|
||||||
|
};
|
||||||
|
rules.push(rule);
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
var index = rules.length - 1;
|
||||||
|
_pendingExpandNotificationRules = true;
|
||||||
|
_pendingNotificationRuleIndex = index;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMuteRuleForApp(appName, desktopEntry) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
|
||||||
|
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
|
||||||
|
if (pattern === "")
|
||||||
|
return;
|
||||||
|
rules.push({
|
||||||
|
enabled: true,
|
||||||
|
field: field,
|
||||||
|
pattern: pattern,
|
||||||
|
matchType: "exact",
|
||||||
|
action: "mute",
|
||||||
|
urgency: "default"
|
||||||
|
});
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppMuted(appName, desktopEntry) {
|
||||||
|
const rules = notificationRules || [];
|
||||||
|
const pat = (desktopEntry && desktopEntry !== "" ? desktopEntry : appName || "").toString().toLowerCase();
|
||||||
|
if (!pat)
|
||||||
|
return false;
|
||||||
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
const r = rules[i];
|
||||||
|
if ((r.action || "").toString().toLowerCase() !== "mute" || r.enabled === false)
|
||||||
|
continue;
|
||||||
|
const field = (r.field || "appName").toString().toLowerCase();
|
||||||
|
const rulePat = (r.pattern || "").toString().toLowerCase();
|
||||||
|
if (!rulePat)
|
||||||
|
continue;
|
||||||
|
const useDesktop = field === "desktopentry";
|
||||||
|
const matches = (useDesktop && desktopEntry) ? (desktopEntry.toString().toLowerCase() === rulePat) : (appName && appName.toString().toLowerCase() === rulePat);
|
||||||
|
if (matches)
|
||||||
|
return true;
|
||||||
|
if (rulePat === pat)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMuteRuleForApp(appName, desktopEntry) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
const app = (appName || "").toString().toLowerCase();
|
||||||
|
const desktop = (desktopEntry || "").toString().toLowerCase();
|
||||||
|
if (!app && !desktop)
|
||||||
|
return;
|
||||||
|
for (let i = rules.length - 1; i >= 0; i--) {
|
||||||
|
const r = rules[i];
|
||||||
|
if ((r.action || "").toString().toLowerCase() !== "mute")
|
||||||
|
continue;
|
||||||
|
const rulePat = (r.pattern || "").toString().toLowerCase();
|
||||||
|
if (!rulePat)
|
||||||
|
continue;
|
||||||
|
if (rulePat === app || rulePat === desktop) {
|
||||||
|
rules.splice(i, 1);
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotificationRule(index, ruleData) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
if (index < 0 || index >= rules.length)
|
||||||
|
return;
|
||||||
|
var existing = rules[index] || {};
|
||||||
|
rules[index] = Object.assign({}, existing, ruleData || {});
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotificationRuleField(index, key, value) {
|
||||||
|
if (key === undefined || key === null || key === "")
|
||||||
|
return;
|
||||||
|
var patch = {};
|
||||||
|
patch[key] = value;
|
||||||
|
updateNotificationRule(index, patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNotificationRule(index) {
|
||||||
|
var rules = JSON.parse(JSON.stringify(notificationRules || []));
|
||||||
|
if (index < 0 || index >= rules.length)
|
||||||
|
return;
|
||||||
|
rules.splice(index, 1);
|
||||||
|
notificationRules = rules;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultNotificationRules() {
|
||||||
|
return Spec.SPEC.notificationRules.def;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetNotificationRules() {
|
||||||
|
notificationRules = JSON.parse(JSON.stringify(Spec.SPEC.notificationRules.def));
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function getDefaultAppIdSubstitutions() {
|
function getDefaultAppIdSubstitutions() {
|
||||||
return Spec.SPEC.appIdSubstitutions.def;
|
return Spec.SPEC.appIdSubstitutions.def;
|
||||||
}
|
}
|
||||||
@@ -2159,19 +2343,40 @@ Singleton {
|
|||||||
Theme.reloadCustomThemeVariant();
|
Theme.reloadCustomThemeVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegistryThemeMultiVariant(themeId, defaults) {
|
function getRegistryThemeMultiVariant(themeId, defaults, mode) {
|
||||||
var stored = registryThemeVariants[themeId];
|
var stored = registryThemeVariants[themeId];
|
||||||
if (stored && typeof stored === "object")
|
if (!stored || typeof stored !== "object")
|
||||||
return stored;
|
return defaults || {};
|
||||||
return defaults || {};
|
if ((stored.dark && typeof stored.dark === "object") || (stored.light && typeof stored.light === "object")) {
|
||||||
|
if (!mode)
|
||||||
|
return stored.dark || stored.light || defaults || {};
|
||||||
|
var modeData = stored[mode];
|
||||||
|
if (modeData && typeof modeData === "object")
|
||||||
|
return modeData;
|
||||||
|
return defaults || {};
|
||||||
|
}
|
||||||
|
return stored;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRegistryThemeMultiVariant(themeId, flavor, accent) {
|
function setRegistryThemeMultiVariant(themeId, flavor, accent, mode) {
|
||||||
var variants = JSON.parse(JSON.stringify(registryThemeVariants));
|
var variants = JSON.parse(JSON.stringify(registryThemeVariants));
|
||||||
variants[themeId] = {
|
var existing = variants[themeId];
|
||||||
|
var perMode = {};
|
||||||
|
if (existing && typeof existing === "object") {
|
||||||
|
if ((existing.dark && typeof existing.dark === "object") || (existing.light && typeof existing.light === "object")) {
|
||||||
|
perMode = existing;
|
||||||
|
} else if (typeof existing.flavor === "string") {
|
||||||
|
perMode.dark = {
|
||||||
|
flavor: existing.flavor,
|
||||||
|
accent: existing.accent || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perMode[mode || "dark"] = {
|
||||||
flavor: flavor,
|
flavor: flavor,
|
||||||
accent: accent
|
accent: accent
|
||||||
};
|
};
|
||||||
|
variants[themeId] = perMode;
|
||||||
registryThemeVariants = variants;
|
registryThemeVariants = variants;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
if (typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
@@ -2371,6 +2576,13 @@ Singleton {
|
|||||||
|
|
||||||
property alias settingsFile: settingsFile
|
property alias settingsFile: settingsFile
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: settingsFileReloadDebounce
|
||||||
|
interval: 50
|
||||||
|
onTriggered: settingsFile.reload()
|
||||||
|
repeat: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
@@ -2378,7 +2590,14 @@ Singleton {
|
|||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
|
onFileChanged: {
|
||||||
|
if (_selfWrite) {
|
||||||
|
_selfWrite = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settingsFileReloadDebounce.restart();
|
||||||
|
}
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
if (isGreeterMode)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ Singleton {
|
|||||||
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
|
||||||
switchTheme(SettingsData.currentThemeName, false, false);
|
switchTheme(SettingsData.currentThemeName, false, false);
|
||||||
|
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
|
||||||
|
SettingsData.updateCosmicThemeMode(currentIsLight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
|
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
|
||||||
@@ -776,6 +778,53 @@ Singleton {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property int notificationAnimationBaseDuration: {
|
||||||
|
if (typeof SettingsData === "undefined")
|
||||||
|
return 200;
|
||||||
|
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.None)
|
||||||
|
return 0;
|
||||||
|
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.Custom)
|
||||||
|
return SettingsData.notificationCustomAnimationDuration;
|
||||||
|
const presetMap = [0, 200, 400, 600];
|
||||||
|
return presetMap[SettingsData.notificationAnimationSpeed] ?? 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int notificationEnterDuration: {
|
||||||
|
const base = notificationAnimationBaseDuration;
|
||||||
|
return base === 0 ? 0 : Math.round(base * 0.875);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int notificationExitDuration: {
|
||||||
|
const base = notificationAnimationBaseDuration;
|
||||||
|
return base === 0 ? 0 : Math.round(base * 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int notificationExpandDuration: {
|
||||||
|
const base = notificationAnimationBaseDuration;
|
||||||
|
return base === 0 ? 0 : Math.round(base * 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int notificationCollapseDuration: {
|
||||||
|
const base = notificationAnimationBaseDuration;
|
||||||
|
return base === 0 ? 0 : Math.round(base * 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real notificationIconSizeNormal: 56
|
||||||
|
readonly property real notificationIconSizeCompact: 48
|
||||||
|
readonly property real notificationExpandedIconSizeNormal: 48
|
||||||
|
readonly property real notificationExpandedIconSizeCompact: 40
|
||||||
|
readonly property real notificationActionMinWidth: 48
|
||||||
|
readonly property real notificationButtonCornerRadius: cornerRadius / 2
|
||||||
|
readonly property real notificationHoverRevealMargin: spacingXL
|
||||||
|
readonly property real notificationContentSpacing: spacingXS
|
||||||
|
readonly property real notificationCardPadding: spacingM
|
||||||
|
readonly property real notificationCardPaddingCompact: spacingS
|
||||||
|
|
||||||
|
readonly property real stateLayerHover: 0.08
|
||||||
|
readonly property real stateLayerFocus: 0.12
|
||||||
|
readonly property real stateLayerPressed: 0.12
|
||||||
|
readonly property real stateLayerDrag: 0.16
|
||||||
|
|
||||||
readonly property int popoutAnimationDuration: {
|
readonly property int popoutAnimationDuration: {
|
||||||
if (typeof SettingsData === "undefined")
|
if (typeof SettingsData === "undefined")
|
||||||
return 150;
|
return 150;
|
||||||
@@ -916,6 +965,9 @@ Singleton {
|
|||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
PortalService.setLightMode(light);
|
PortalService.setLightMode(light);
|
||||||
}
|
}
|
||||||
|
if (typeof SettingsData !== "undefined") {
|
||||||
|
SettingsData.updateCosmicThemeMode(light);
|
||||||
|
}
|
||||||
generateSystemThemesFromCurrentTheme();
|
generateSystemThemesFromCurrentTheme();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -971,7 +1023,7 @@ 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) : modeDefaults;
|
const stored = 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);
|
||||||
@@ -1365,8 +1417,8 @@ 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) : darkDefaults;
|
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
|
||||||
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults) : lightDefaults;
|
const storedLight = 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 || "";
|
||||||
|
|||||||
@@ -32,8 +32,15 @@ function markdownToHtml(text) {
|
|||||||
return `\x00INLINECODE${inlineIndex++}\x00`;
|
return `\x00INLINECODE${inlineIndex++}\x00`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now process everything else
|
// Extract plain URLs before escaping so & in query strings is preserved
|
||||||
// Escape HTML entities (but not in code blocks)
|
const urls = [];
|
||||||
|
let urlIndex = 0;
|
||||||
|
html = html.replace(/(^|[\s])((?:https?|file):\/\/[^\s]+)/gm, (match, prefix, url) => {
|
||||||
|
urls.push(url);
|
||||||
|
return prefix + `\x00URL${urlIndex++}\x00`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape HTML entities (but not in code blocks or URLs)
|
||||||
html = html.replace(/&/g, '&')
|
html = html.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>');
|
.replace(/>/g, '>');
|
||||||
@@ -64,8 +71,12 @@ function markdownToHtml(text) {
|
|||||||
return '<ul>' + match + '</ul>';
|
return '<ul>' + match + '</ul>';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
|
// Restore extracted URLs as anchor tags (preserves raw & in href)
|
||||||
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
|
html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
|
||||||
|
const url = urls[parseInt(index)];
|
||||||
|
const display = url.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
return `<a href="${url}">${display}</a>`;
|
||||||
|
});
|
||||||
|
|
||||||
// Restore code blocks and inline code BEFORE line break processing
|
// Restore code blocks and inline code BEFORE line break processing
|
||||||
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
|
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ var SPEC = {
|
|||||||
|
|
||||||
vpnLastConnected: { def: "" },
|
vpnLastConnected: { def: "" },
|
||||||
|
|
||||||
deviceMaxVolumes: { def: {} }
|
deviceMaxVolumes: { def: {} },
|
||||||
|
hiddenOutputDeviceNames: { def: [] },
|
||||||
|
hiddenInputDeviceNames: { def: [] }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ var SPEC = {
|
|||||||
dankLauncherV2BorderThickness: { def: 2 },
|
dankLauncherV2BorderThickness: { def: 2 },
|
||||||
dankLauncherV2BorderColor: { def: "primary" },
|
dankLauncherV2BorderColor: { def: "primary" },
|
||||||
dankLauncherV2ShowFooter: { def: true },
|
dankLauncherV2ShowFooter: { def: true },
|
||||||
|
dankLauncherV2UnloadOnClose: { def: false },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
@@ -296,6 +297,8 @@ var SPEC = {
|
|||||||
dockShowOverflowBadge: { def: true },
|
dockShowOverflowBadge: { def: true },
|
||||||
|
|
||||||
notificationOverlayEnabled: { def: false },
|
notificationOverlayEnabled: { def: false },
|
||||||
|
notificationPopupShadowEnabled: { def: true },
|
||||||
|
notificationPopupPrivacyMode: { def: false },
|
||||||
overviewRows: { def: 2, persist: false },
|
overviewRows: { def: 2, persist: false },
|
||||||
overviewColumns: { def: 5, persist: false },
|
overviewColumns: { def: 5, persist: false },
|
||||||
overviewScale: { def: 0.16, persist: false },
|
overviewScale: { def: 0.16, persist: false },
|
||||||
@@ -324,12 +327,15 @@ var SPEC = {
|
|||||||
notificationTimeoutCritical: { def: 0 },
|
notificationTimeoutCritical: { def: 0 },
|
||||||
notificationCompactMode: { def: false },
|
notificationCompactMode: { def: false },
|
||||||
notificationPopupPosition: { def: 0 },
|
notificationPopupPosition: { def: 0 },
|
||||||
|
notificationAnimationSpeed: { def: 1 },
|
||||||
|
notificationCustomAnimationDuration: { def: 400 },
|
||||||
notificationHistoryEnabled: { def: true },
|
notificationHistoryEnabled: { def: true },
|
||||||
notificationHistoryMaxCount: { def: 50 },
|
notificationHistoryMaxCount: { def: 50 },
|
||||||
notificationHistoryMaxAgeDays: { def: 7 },
|
notificationHistoryMaxAgeDays: { def: 7 },
|
||||||
notificationHistorySaveLow: { def: true },
|
notificationHistorySaveLow: { def: true },
|
||||||
notificationHistorySaveNormal: { def: true },
|
notificationHistorySaveNormal: { def: true },
|
||||||
notificationHistorySaveCritical: { def: true },
|
notificationHistorySaveCritical: { def: true },
|
||||||
|
notificationRules: { def: [] },
|
||||||
|
|
||||||
osdAlwaysShowValue: { def: false },
|
osdAlwaysShowValue: { def: false },
|
||||||
osdPosition: { def: 5 },
|
osdPosition: { def: 5 },
|
||||||
@@ -468,6 +474,8 @@ var SPEC = {
|
|||||||
desktopWidgetGroups: { def: [] },
|
desktopWidgetGroups: { def: [] },
|
||||||
|
|
||||||
builtInPluginSettings: { def: {} },
|
builtInPluginSettings: { def: {} },
|
||||||
|
clipboardEnterToPaste: { def: false },
|
||||||
|
|
||||||
launcherPluginVisibility: { def: {} },
|
launcherPluginVisibility: { def: {} },
|
||||||
launcherPluginOrder: { def: [] }
|
launcherPluginOrder: { def: [] }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -251,13 +251,20 @@ Item {
|
|||||||
active: false
|
active: false
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.dankDashPopoutLoader = dankDashPopoutLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (item) {
|
||||||
|
PopoutService.dankDashPopout = item;
|
||||||
|
PopoutService._onDankDashPopoutLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceComponent: Component {
|
sourceComponent: Component {
|
||||||
DankDashPopout {
|
DankDashPopout {
|
||||||
id: dankDashPopout
|
id: dankDashPopout
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
PopoutService.dankDashPopout = dankDashPopout;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ Item {
|
|||||||
if (CompositorService.isNiri && NiriService.currentOutput) {
|
if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||||
return NiriService.currentOutput;
|
return NiriService.currentOutput;
|
||||||
}
|
}
|
||||||
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) {
|
if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && I3.workspaces?.values) {
|
||||||
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
|
||||||
return focusedWs?.monitor?.name || "";
|
return focusedWs?.monitor?.name || "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Column {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: codenameText
|
id: codenameText
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Spicy Miso"
|
text: "Saffron Bloom"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -74,7 +74,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "Desktop widgets, theme registry, native clipboard & more"
|
text: "New launcher, enhanced plugin system, KDE Connect, & more"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
@@ -108,67 +108,76 @@ Column {
|
|||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "widgets"
|
iconName: "space_dashboard"
|
||||||
title: "Desktop Widgets"
|
title: "Dank Launcher V2"
|
||||||
description: "Widgets on your desktop"
|
description: "New capabilities & plugins"
|
||||||
onClicked: PopoutService.openSettingsWithTab("desktop_widgets")
|
onClicked: PopoutService.openDankLauncherV2()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "palette"
|
iconName: "smartphone"
|
||||||
title: "Theme Registry"
|
title: "Phone Connect"
|
||||||
description: "Community themes"
|
description: "KDE Connect & Valent"
|
||||||
onClicked: PopoutService.openSettingsWithTab("theme")
|
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "content_paste"
|
iconName: "monitor_heart"
|
||||||
title: "Native Clipboard"
|
title: "System Monitor"
|
||||||
description: "Zero-dependency history"
|
description: "Redesigned process list"
|
||||||
onClicked: PopoutService.openSettingsWithTab("clipboard")
|
onClicked: PopoutService.showProcessListModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "display_settings"
|
iconName: "window"
|
||||||
title: "Monitor Config"
|
title: "Window Rules"
|
||||||
description: "Full display setup"
|
description: "niri window rule manager"
|
||||||
onClicked: PopoutService.openSettingsWithTab("display_config")
|
visible: CompositorService.isNiri
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("window_rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "notifications_active"
|
iconName: "notifications_active"
|
||||||
title: "Notifications"
|
title: "Enhanced Notifications"
|
||||||
description: "History & gestures"
|
description: "Configurable rules & styling"
|
||||||
|
visible: !CompositorService.isNiri
|
||||||
onClicked: PopoutService.openSettingsWithTab("notifications")
|
onClicked: PopoutService.openSettingsWithTab("notifications")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "healing"
|
iconName: "dock_to_bottom"
|
||||||
title: "DMS Doctor"
|
title: "Dock Enhancements"
|
||||||
description: "Diagnose issues"
|
description: "Bar dock widget & more"
|
||||||
onClicked: FirstLaunchService.showDoctor()
|
onClicked: PopoutService.openSettingsWithTab("dock")
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "keyboard"
|
iconName: "volume_up"
|
||||||
title: "Keybinds Editor"
|
title: "Audio Aliases"
|
||||||
description: "niri, Hyprland, & MangoWC"
|
description: "Custom device names"
|
||||||
visible: KeybindsService.available
|
onClicked: PopoutService.openSettingsWithTab("audio")
|
||||||
onClicked: PopoutService.openSettingsWithTab("keybinds")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogFeatureCard {
|
ChangelogFeatureCard {
|
||||||
width: (parent.width - Theme.spacingS) / 2
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
iconName: "search"
|
iconName: "extension"
|
||||||
title: "Settings Search"
|
title: "Enhanced Plugin System"
|
||||||
description: "Find settings fast"
|
description: "Enables new types of plugins"
|
||||||
onClicked: PopoutService.openSettings()
|
onClicked: PopoutService.openSettingsWithTab("plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangelogFeatureCard {
|
||||||
|
width: (parent.width - Theme.spacingS) / 2
|
||||||
|
iconName: "light_mode"
|
||||||
|
title: "Auto Light/Dark"
|
||||||
|
description: "Automatic mode switching"
|
||||||
|
onClicked: PopoutService.openSettingsWithTab("theme")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,26 +230,21 @@ Column {
|
|||||||
|
|
||||||
ChangelogUpgradeNote {
|
ChangelogUpgradeNote {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "Ghostty theme path changed to ~/.config/ghostty/themes/danktheme"
|
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogUpgradeNote {
|
ChangelogUpgradeNote {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "VS Code theme reinstall required"
|
text: "Plugin API updated — third-party plugins may need updates"
|
||||||
}
|
|
||||||
|
|
||||||
ChangelogUpgradeNote {
|
|
||||||
width: parent.width
|
|
||||||
text: "Clipboard history migration available from cliphist"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
// StyledText {
|
||||||
text: "See full release notes for migration steps"
|
// text: "See full release notes for migration steps"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
// font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
// color: Theme.surfaceVariantText
|
||||||
width: parent.width
|
// width: parent.width
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ FloatingWindow {
|
|||||||
iconName: "open_in_new"
|
iconName: "open_in_new"
|
||||||
backgroundColor: Theme.surfaceContainerHighest
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-2-release")
|
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
|
||||||
}
|
}
|
||||||
|
|
||||||
DankButton {
|
DankButton {
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ Item {
|
|||||||
|
|
||||||
sourceComponent: ClipboardKeyboardHints {
|
sourceComponent: ClipboardKeyboardHints {
|
||||||
wtypeAvailable: modal.wtypeAvailable
|
wtypeAvailable: modal.wtypeAvailable
|
||||||
|
enterToPaste: SettingsData.clipboardEnterToPaste
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
@@ -133,7 +134,11 @@ QtObject {
|
|||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
if (ClipboardService.keyboardNavigationActive) {
|
if (ClipboardService.keyboardNavigationActive) {
|
||||||
modal.pasteSelected();
|
if (SettingsData.clipboardEnterToPaste) {
|
||||||
|
copySelected();
|
||||||
|
} else {
|
||||||
|
modal.pasteSelected();
|
||||||
|
}
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -144,7 +149,11 @@ QtObject {
|
|||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
case Qt.Key_Enter:
|
case Qt.Key_Enter:
|
||||||
copySelected();
|
if (SettingsData.clipboardEnterToPaste) {
|
||||||
|
modal.pasteSelected();
|
||||||
|
} else {
|
||||||
|
copySelected();
|
||||||
|
}
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Delete:
|
case Qt.Key_Delete:
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ Rectangle {
|
|||||||
id: keyboardHints
|
id: keyboardHints
|
||||||
|
|
||||||
property bool wtypeAvailable: false
|
property bool wtypeAvailable: false
|
||||||
readonly property string hintsText: wtypeAvailable ? I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close") : I18n.tr("Shift+Del: Clear All • Esc: Close")
|
property bool enterToPaste: false
|
||||||
|
readonly property string hintsText: {
|
||||||
|
if (!wtypeAvailable)
|
||||||
|
return I18n.tr("Shift+Del: Clear All • Esc: Close");
|
||||||
|
return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
|
||||||
|
}
|
||||||
|
|
||||||
height: ClipboardConstants.keyboardHintsHeight
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
@@ -21,7 +26,7 @@ Rectangle {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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
|
||||||
@@ -378,11 +377,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: root.backgroundColor
|
color: root.backgroundColor
|
||||||
borderColor: root.borderColor
|
border.color: root.borderColor
|
||||||
borderWidth: root.borderWidth
|
border.width: root.borderWidth
|
||||||
radius: root.cornerRadius
|
radius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ Item {
|
|||||||
signal viewModeChanged(string sectionId, string mode)
|
signal viewModeChanged(string sectionId, string mode)
|
||||||
signal searchQueryRequested(string query)
|
signal searchQueryRequested(string query)
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (!active) {
|
||||||
|
sections = [];
|
||||||
|
flatModel = [];
|
||||||
|
selectedItem = null;
|
||||||
|
_clearModeCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onSortAppsAlphabeticallyChanged() {
|
function onSortAppsAlphabeticallyChanged() {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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
|
||||||
@@ -14,10 +13,14 @@ Item {
|
|||||||
property bool spotlightOpen: false
|
property bool spotlightOpen: false
|
||||||
property bool keyboardActive: false
|
property bool keyboardActive: false
|
||||||
property bool contentVisible: false
|
property bool contentVisible: false
|
||||||
property alias spotlightContent: launcherContent
|
property var spotlightContent: launcherContentLoader.item
|
||||||
property bool openedFromOverview: false
|
property bool openedFromOverview: false
|
||||||
property bool isClosing: false
|
property bool isClosing: false
|
||||||
property bool _windowEnabled: true
|
property bool _windowEnabled: true
|
||||||
|
property bool _pendingInitialize: false
|
||||||
|
property string _pendingQuery: ""
|
||||||
|
property string _pendingMode: ""
|
||||||
|
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||||
|
|
||||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
readonly property var effectiveScreen: launcherWindow.screen
|
readonly property var effectiveScreen: launcherWindow.screen
|
||||||
@@ -76,7 +79,22 @@ Item {
|
|||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
|
_pendingQuery = query || "";
|
||||||
|
_pendingMode = mode || "";
|
||||||
|
_pendingInitialize = true;
|
||||||
|
contentVisible = true;
|
||||||
|
launcherContentLoader.active = true;
|
||||||
|
|
||||||
|
if (spotlightContent) {
|
||||||
|
_initializeAndShow(_pendingQuery, _pendingMode);
|
||||||
|
_pendingInitialize = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _initializeAndShow(query, mode) {
|
function _initializeAndShow(query, mode) {
|
||||||
|
if (!spotlightContent)
|
||||||
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
|
|
||||||
@@ -122,7 +140,7 @@ Item {
|
|||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
_initializeAndShow("");
|
_ensureContentLoadedAndInitialize("", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithQuery(query) {
|
function showWithQuery(query) {
|
||||||
@@ -140,7 +158,7 @@ Item {
|
|||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
_initializeAndShow(query);
|
_ensureContentLoadedAndInitialize(query, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -177,7 +195,7 @@ Item {
|
|||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
_initializeAndShow("", mode);
|
_ensureContentLoadedAndInitialize("", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWithMode(mode) {
|
function toggleWithMode(mode) {
|
||||||
@@ -202,6 +220,8 @@ Item {
|
|||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
|
if (root.unloadContentOnClose)
|
||||||
|
launcherContentLoader.active = false;
|
||||||
dialogClosed();
|
dialogClosed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +284,7 @@ Item {
|
|||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: launcherWindow
|
id: launcherWindow
|
||||||
visible: root._windowEnabled
|
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
@@ -357,11 +377,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: root.backgroundColor
|
color: root.backgroundColor
|
||||||
borderColor: root.borderColor
|
border.color: root.borderColor
|
||||||
borderWidth: root.borderWidth
|
border.width: root.borderWidth
|
||||||
radius: root.cornerRadius
|
radius: root.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,10 +394,22 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: keyboardActive
|
focus: keyboardActive
|
||||||
|
|
||||||
LauncherContent {
|
Loader {
|
||||||
id: launcherContent
|
id: launcherContentLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
parentModal: root
|
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: LauncherContent {
|
||||||
|
focus: true
|
||||||
|
parentModal: root
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (root._pendingInitialize) {
|
||||||
|
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||||
|
root._pendingInitialize = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
|||||||
_hName: "",
|
_hName: "",
|
||||||
_hSub: "",
|
_hSub: "",
|
||||||
_hRich: false,
|
_hRich: false,
|
||||||
_preScored: undefined
|
_preScored: item._preScored
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ function transformPluginItem(item, pluginId, selectLabel) {
|
|||||||
_hName: "",
|
_hName: "",
|
||||||
_hSub: "",
|
_hSub: "",
|
||||||
_hRich: false,
|
_hRich: false,
|
||||||
_preScored: undefined
|
_preScored: item._preScored
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ FocusScope {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: -Theme.cornerRadius
|
anchors.topMargin: -Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, g
|
|||||||
return bounds.start + newPosInSection;
|
return bounds.start + newPosInSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentRow = Math.floor(posInSection / cols);
|
||||||
|
var lastRow = Math.floor((bounds.count - 1) / cols);
|
||||||
|
if (currentRow < lastRow) {
|
||||||
|
return bounds.start + bounds.count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
|
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
|
||||||
return nextSection !== -1 ? nextSection : selectedFlatIndex;
|
return nextSection !== -1 ? nextSection : selectedFlatIndex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,16 @@ Item {
|
|||||||
var rowIndex = _flatIndexToRowMap[index];
|
var rowIndex = _flatIndexToRowMap[index];
|
||||||
if (rowIndex === undefined)
|
if (rowIndex === undefined)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
|
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
|
||||||
|
|
||||||
|
if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
|
||||||
|
var rowY = _cumulativeHeights[rowIndex];
|
||||||
|
var scrollY = mainListView.contentY - mainListView.originY;
|
||||||
|
if (rowY < scrollY + stickyHeader.height) {
|
||||||
|
mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedItemPosition() {
|
function getSelectedItemPosition() {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ function scoreItems(items, query, getFrecencyFn) {
|
|||||||
var item = items[i]
|
var item = items[i]
|
||||||
var itemScore
|
var itemScore
|
||||||
|
|
||||||
if (item._preScored !== undefined) {
|
if (query && item._preScored !== undefined) {
|
||||||
itemScore = item._preScored
|
itemScore = item._preScored
|
||||||
} else {
|
} else {
|
||||||
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Rectangle {
|
|||||||
if (!path)
|
if (!path)
|
||||||
return false;
|
return false;
|
||||||
var ext = path.split('.').pop().toLowerCase();
|
var ext = path.split('.').pop().toLowerCase();
|
||||||
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0;
|
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ FocusScope {
|
|||||||
showDirsFirst: true
|
showDirsFirst: true
|
||||||
showDotAndDotDot: false
|
showDotAndDotDot: false
|
||||||
showHidden: root.showHiddenFiles
|
showHidden: root.showHiddenFiles
|
||||||
|
caseSensitive: false
|
||||||
nameFilters: fileExtensions
|
nameFilters: fileExtensions
|
||||||
showFiles: true
|
showFiles: true
|
||||||
showDirs: true
|
showDirs: true
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ StyledRect {
|
|||||||
function determineFileType(fileName) {
|
function determineFileType(fileName) {
|
||||||
const ext = getFileExtension(fileName);
|
const ext = getFileExtension(fileName);
|
||||||
|
|
||||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
|
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||||
if (imageExts.includes(ext)) {
|
if (imageExts.includes(ext)) {
|
||||||
return "image";
|
return "image";
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ StyledRect {
|
|||||||
id: gridPreviewImage
|
id: gridPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 2
|
anchors.margins: 2
|
||||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
|
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||||
property int weExtIndex: 0
|
property int weExtIndex: 0
|
||||||
property string imagePath: {
|
property string imagePath: {
|
||||||
if (weMode && delegateRoot.fileIsDir)
|
if (weMode && delegateRoot.fileIsDir)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ StyledRect {
|
|||||||
function determineFileType(fileName) {
|
function determineFileType(fileName) {
|
||||||
const ext = getFileExtension(fileName);
|
const ext = getFileExtension(fileName);
|
||||||
|
|
||||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
|
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||||
if (imageExts.includes(ext)) {
|
if (imageExts.includes(ext)) {
|
||||||
return "image";
|
return "image";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
@@ -18,7 +20,11 @@ DankModal {
|
|||||||
modalHeight: _maxH
|
modalHeight: _maxH
|
||||||
onBackgroundClicked: close()
|
onBackgroundClicked: close()
|
||||||
onOpened: {
|
onOpened: {
|
||||||
Qt.callLater(() => modalFocusScope.forceActiveFocus());
|
Qt.callLater(() => {
|
||||||
|
modalFocusScope.forceActiveFocus();
|
||||||
|
if (contentLoader.item?.searchField)
|
||||||
|
contentLoader.item.searchField.forceActiveFocus();
|
||||||
|
});
|
||||||
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
|
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
|
||||||
KeybindsService.loadCheatsheet();
|
KeybindsService.loadCheatsheet();
|
||||||
}
|
}
|
||||||
@@ -63,17 +69,39 @@ DankModal {
|
|||||||
content: Component {
|
content: Component {
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
property alias searchField: searchField
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingL
|
anchors.margins: Theme.spacingL
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
StyledText {
|
RowLayout {
|
||||||
text: KeybindsService.cheatsheet.title || "Keybinds"
|
width: parent.width
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.Bold
|
StyledText {
|
||||||
color: Theme.primary
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
text: KeybindsService.cheatsheet.title || "Keybinds"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: searchField
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
leftIconName: "search"
|
||||||
|
onTextEdited: searchDebounce.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounce
|
||||||
|
interval: 50
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
@@ -87,17 +115,26 @@ DankModal {
|
|||||||
Component.onCompleted: root.activeFlickable = mainFlickable
|
Component.onCompleted: root.activeFlickable = mainFlickable
|
||||||
|
|
||||||
property var rawBinds: KeybindsService.cheatsheet.binds || {}
|
property var rawBinds: KeybindsService.cheatsheet.binds || {}
|
||||||
property var categories: {
|
|
||||||
|
function generateCategories(query) {
|
||||||
|
const lowerQuery = query ? query.toLowerCase().trim() : "";
|
||||||
const processed = {};
|
const processed = {};
|
||||||
|
|
||||||
for (const cat in rawBinds) {
|
for (const cat in rawBinds) {
|
||||||
const binds = rawBinds[cat];
|
const binds = rawBinds[cat];
|
||||||
|
const catLower = cat.toLowerCase();
|
||||||
const subcats = {};
|
const subcats = {};
|
||||||
let hasSubcats = false;
|
let hasSubcats = false;
|
||||||
|
|
||||||
for (let i = 0; i < binds.length; i++) {
|
for (let i = 0; i < binds.length; i++) {
|
||||||
const bind = binds[i];
|
const bind = binds[i];
|
||||||
|
const keyLower = bind.key.toLowerCase();
|
||||||
|
const descLower = bind.desc.toLowerCase();
|
||||||
|
const actionLower = bind.action.toLowerCase();
|
||||||
|
if (!(lowerQuery.length === 0 || keyLower.includes(lowerQuery) || descLower.includes(lowerQuery) || catLower.includes(lowerQuery) || actionLower.includes(lowerQuery)))
|
||||||
|
continue;
|
||||||
if (bind.hideOnOverlay)
|
if (bind.hideOnOverlay)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (bind.subcat) {
|
if (bind.subcat) {
|
||||||
hasSubcats = true;
|
hasSubcats = true;
|
||||||
if (!subcats[bind.subcat])
|
if (!subcats[bind.subcat])
|
||||||
@@ -119,9 +156,11 @@ DankModal {
|
|||||||
subcatKeys: Object.keys(subcats)
|
subcatKeys: Object.keys(subcats)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
property var categoryKeys: Object.keys(categories)
|
|
||||||
|
property var categories: generateCategories("");
|
||||||
|
|
||||||
function estimateCategoryHeight(catName) {
|
function estimateCategoryHeight(catName) {
|
||||||
const catData = categories[catName];
|
const catData = categories[catName];
|
||||||
@@ -136,6 +175,8 @@ DankModal {
|
|||||||
return 40 + bindCount * 28;
|
return 40 + bindCount * 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var categoryKeys: Object.keys(categories);
|
||||||
|
|
||||||
function distributeCategories(cols) {
|
function distributeCategories(cols) {
|
||||||
const columns = [];
|
const columns = [];
|
||||||
const heights = [];
|
const heights = [];
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ DankModal {
|
|||||||
NotificationService.dismissAllPopups();
|
NotificationService.dismissAllPopups();
|
||||||
}
|
}
|
||||||
|
|
||||||
modalWidth: 500
|
modalWidth: Math.min(500, screenWidth - 48)
|
||||||
modalHeight: 700
|
modalHeight: Math.min(700, screenHeight * 0.85)
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
visible: false
|
visible: false
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ FloatingWindow {
|
|||||||
property int currentTab: 0
|
property int currentTab: 0
|
||||||
property string searchText: ""
|
property string searchText: ""
|
||||||
property string expandedPid: ""
|
property string expandedPid: ""
|
||||||
|
property string processFilter: "all"
|
||||||
property bool shouldHaveFocus: visible
|
property bool shouldHaveFocus: visible
|
||||||
property alias shouldBeVisible: processListModal.visible
|
property alias shouldBeVisible: processListModal.visible
|
||||||
|
|
||||||
@@ -82,9 +83,9 @@ FloatingWindow {
|
|||||||
|
|
||||||
objectName: "processListModal"
|
objectName: "processListModal"
|
||||||
title: I18n.tr("System Monitor", "sysmon window title")
|
title: I18n.tr("System Monitor", "sysmon window title")
|
||||||
minimumSize: Qt.size(750, 550)
|
minimumSize: Qt.size(Math.min(Math.round(Theme.fontSizeMedium * 48), Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 34), Screen.height))
|
||||||
implicitWidth: 1000
|
implicitWidth: Math.round(Theme.fontSizeMedium * 71)
|
||||||
implicitHeight: 720
|
implicitHeight: Math.round(Theme.fontSizeMedium * 51)
|
||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -98,6 +99,8 @@ FloatingWindow {
|
|||||||
closingModal();
|
closingModal();
|
||||||
searchText = "";
|
searchText = "";
|
||||||
expandedPid = "";
|
expandedPid = "";
|
||||||
|
processFilter = "all";
|
||||||
|
processFilterGroup.currentIndex = 0;
|
||||||
if (processesTabLoader.item)
|
if (processesTabLoader.item)
|
||||||
processesTabLoader.item.reset();
|
processesTabLoader.item.reset();
|
||||||
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
|
||||||
@@ -233,7 +236,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 48
|
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.4)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -290,10 +293,10 @@ FloatingWindow {
|
|||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 52
|
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.7)
|
||||||
Layout.leftMargin: Theme.spacingL
|
Layout.leftMargin: Theme.spacingL
|
||||||
Layout.rightMargin: Theme.spacingL
|
Layout.rightMargin: Theme.spacingL
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: 2
|
spacing: 2
|
||||||
@@ -319,14 +322,15 @@ FloatingWindow {
|
|||||||
]
|
]
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 120
|
width: tabRowContent.implicitWidth + Theme.spacingM * 2
|
||||||
height: 44
|
height: Math.round(Theme.fontSizeMedium * 3.1)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
|
||||||
border.color: currentTab === index ? Theme.primary : "transparent"
|
border.color: currentTab === index ? Theme.primary : "transparent"
|
||||||
border.width: currentTab === index ? 1 : 0
|
border.width: currentTab === index ? 1 : 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
id: tabRowContent
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
@@ -368,10 +372,40 @@ FloatingWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankButtonGroup {
|
||||||
|
id: processFilterGroup
|
||||||
|
model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
|
||||||
|
currentIndex: 0
|
||||||
|
checkEnabled: false
|
||||||
|
buttonHeight: Math.round(Theme.fontSizeSmall * 2.6)
|
||||||
|
minButtonWidth: 0
|
||||||
|
buttonPadding: Theme.spacingS
|
||||||
|
textSize: Theme.fontSizeSmall
|
||||||
|
visible: currentTab === 0
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected)
|
||||||
|
return;
|
||||||
|
currentIndex = index;
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
processListModal.processFilter = "all";
|
||||||
|
return;
|
||||||
|
case 1:
|
||||||
|
processListModal.processFilter = "user";
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
processListModal.processFilter = "system";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankTextField {
|
DankTextField {
|
||||||
id: searchField
|
id: searchField
|
||||||
Layout.preferredWidth: 250
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 40
|
Layout.maximumWidth: Math.round(Theme.fontSizeMedium * 18)
|
||||||
|
Layout.minimumWidth: Theme.fontSizeMedium * 4
|
||||||
|
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 2.8)
|
||||||
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
placeholderText: I18n.tr("Search processes...", "process search placeholder")
|
||||||
leftIconName: "search"
|
leftIconName: "search"
|
||||||
showClearButton: true
|
showClearButton: true
|
||||||
@@ -403,6 +437,7 @@ FloatingWindow {
|
|||||||
sourceComponent: ProcessesView {
|
sourceComponent: ProcessesView {
|
||||||
searchText: processListModal.searchText
|
searchText: processListModal.searchText
|
||||||
expandedPid: processListModal.expandedPid
|
expandedPid: processListModal.expandedPid
|
||||||
|
processFilter: processListModal.processFilter
|
||||||
contextMenu: processContextMenu
|
contextMenu: processContextMenu
|
||||||
onExpandedPidChanged: processListModal.expandedPid = expandedPid
|
onExpandedPidChanged: processListModal.expandedPid = expandedPid
|
||||||
}
|
}
|
||||||
@@ -438,7 +473,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 32
|
Layout.preferredHeight: Math.round(Theme.fontSizeSmall * 2.7)
|
||||||
Layout.leftMargin: Theme.spacingL
|
Layout.leftMargin: Theme.spacingL
|
||||||
Layout.rightMargin: Theme.spacingL
|
Layout.rightMargin: Theme.spacingL
|
||||||
Layout.bottomMargin: Theme.spacingM
|
Layout.bottomMargin: Theme.spacingM
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ FloatingWindow {
|
|||||||
browserIcon: "person"
|
browserIcon: "person"
|
||||||
browserType: "profile"
|
browserType: "profile"
|
||||||
showHiddenFiles: true
|
showHiddenFiles: true
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||||
onFileSelected: path => {
|
onFileSelected: path => {
|
||||||
PortalService.setProfileImage(path);
|
PortalService.setProfileImage(path);
|
||||||
close();
|
close();
|
||||||
@@ -152,7 +152,7 @@ FloatingWindow {
|
|||||||
browserIcon: "wallpaper"
|
browserIcon: "wallpaper"
|
||||||
browserType: "wallpaper"
|
browserType: "wallpaper"
|
||||||
showHiddenFiles: true
|
showHiddenFiles: true
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||||
onFileSelected: path => {
|
onFileSelected: path => {
|
||||||
SessionData.setWallpaper(path);
|
SessionData.setWallpaper(path);
|
||||||
close();
|
close();
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ Rectangle {
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "display_config",
|
"id": "display_config",
|
||||||
"text": I18n.tr("Configuration") + " (Beta)",
|
"text": I18n.tr("Configuration"),
|
||||||
"icon": "display_settings",
|
"icon": "display_settings",
|
||||||
"tabIndex": 24
|
"tabIndex": 24
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ FloatingWindow {
|
|||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
topPadding: Theme.spacingM
|
topPadding: Theme.spacingM
|
||||||
bottomPadding: Theme.spacingXS
|
bottomPadding: Theme.spacingXS
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
component CheckboxRow: Row {
|
component CheckboxRow: Row {
|
||||||
@@ -371,6 +373,9 @@ FloatingWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
hide();
|
hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
@@ -401,12 +406,16 @@ FloatingWindow {
|
|||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Configure match criteria and actions")
|
text: I18n.tr("Configure match criteria and actions")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: Theme.surfaceTextMedium
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,6 +559,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Output")
|
text: I18n.tr("Output")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -575,6 +586,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Workspace")
|
text: I18n.tr("Workspace")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -606,6 +619,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Column Width")
|
text: I18n.tr("Column Width")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -631,6 +646,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Window Height")
|
text: I18n.tr("Window Height")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -710,6 +727,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Block Out From")
|
text: I18n.tr("Block Out From")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
DankDropdown {
|
DankDropdown {
|
||||||
@@ -730,6 +749,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Column Display")
|
text: I18n.tr("Column Display")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
DankDropdown {
|
DankDropdown {
|
||||||
@@ -802,6 +823,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Min W")
|
text: I18n.tr("Min W")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -827,6 +850,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Max W")
|
text: I18n.tr("Max W")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -852,6 +877,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Min H")
|
text: I18n.tr("Min H")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -877,6 +904,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Max H")
|
text: I18n.tr("Max H")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -960,6 +989,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Size")
|
text: I18n.tr("Size")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -985,6 +1016,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Move")
|
text: I18n.tr("Move")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -1016,6 +1049,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Monitor")
|
text: I18n.tr("Monitor")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
@@ -1041,6 +1076,8 @@ FloatingWindow {
|
|||||||
text: I18n.tr("Workspace")
|
text: I18n.tr("Workspace")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
}
|
}
|
||||||
|
|
||||||
InputField {
|
InputField {
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ DankPopout {
|
|||||||
property alias launcherContent: launcherContent
|
property alias launcherContent: launcherContent
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
@@ -68,35 +65,6 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: [
|
|
||||||
{
|
|
||||||
"margin": -3,
|
|
||||||
"color": Qt.rgba(0, 0, 0, 0.05),
|
|
||||||
"z": -3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"margin": -2,
|
|
||||||
"color": Qt.rgba(0, 0, 0, 0.08),
|
|
||||||
"z": -2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"margin": 0,
|
|
||||||
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
|
|
||||||
"z": -1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: modelData.margin
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + Math.abs(modelData.margin)
|
|
||||||
border.color: modelData.color
|
|
||||||
border.width: 0
|
|
||||||
z: modelData.z
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ Item {
|
|||||||
readonly property color dimColor: Theme.surfaceVariantText
|
readonly property color dimColor: Theme.surfaceVariantText
|
||||||
|
|
||||||
property string currentGpuPciIdRef: ""
|
property string currentGpuPciIdRef: ""
|
||||||
|
property var activeModuleRefs: []
|
||||||
|
|
||||||
property var cpuHistory: []
|
property var cpuHistory: []
|
||||||
property var memHistory: []
|
property var memHistory: []
|
||||||
@@ -140,12 +141,13 @@ Item {
|
|||||||
modules.push("disk", "diskmounts");
|
modules.push("disk", "diskmounts");
|
||||||
if (showTopProcesses)
|
if (showTopProcesses)
|
||||||
modules.push("processes");
|
modules.push("processes");
|
||||||
|
activeModuleRefs = modules;
|
||||||
DgopService.addRef(modules);
|
DgopService.addRef(modules);
|
||||||
updateGpuRef();
|
updateGpuRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
DgopService.removeRef();
|
DgopService.removeRef(activeModuleRefs);
|
||||||
if (currentGpuPciIdRef)
|
if (currentGpuPciIdRef)
|
||||||
DgopService.removeGpuPciId(currentGpuPciIdRef);
|
DgopService.removeGpuPciId(currentGpuPciIdRef);
|
||||||
}
|
}
|
||||||
@@ -153,8 +155,13 @@ Item {
|
|||||||
onShowGpuTempChanged: updateGpuRef()
|
onShowGpuTempChanged: updateGpuRef()
|
||||||
onSelectedGpuPciIdChanged: updateGpuRef()
|
onSelectedGpuPciIdChanged: updateGpuRef()
|
||||||
onShowTopProcessesChanged: {
|
onShowTopProcessesChanged: {
|
||||||
if (showTopProcesses)
|
if (showTopProcesses) {
|
||||||
|
activeModuleRefs = activeModuleRefs.concat(["processes"]);
|
||||||
DgopService.addRef(["processes"]);
|
DgopService.addRef(["processes"]);
|
||||||
|
} else {
|
||||||
|
DgopService.removeRef(["processes"]);
|
||||||
|
activeModuleRefs = activeModuleRefs.filter(m => m !== "processes");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGpuRef() {
|
function updateGpuRef() {
|
||||||
|
|||||||
@@ -945,22 +945,31 @@ Column {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
function tryCreatePluginInstance() {
|
||||||
Qt.callLater(() => {
|
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
|
||||||
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
|
if (!pluginComponent)
|
||||||
if (pluginComponent) {
|
return false;
|
||||||
const instance = pluginComponent.createObject(null, {
|
try {
|
||||||
"pluginId": pluginId,
|
const instance = pluginComponent.createObject(null, {
|
||||||
"pluginService": PluginService,
|
"pluginId": pluginId,
|
||||||
"visible": false,
|
"pluginService": PluginService,
|
||||||
"width": 0,
|
"visible": false,
|
||||||
"height": 0
|
"width": 0,
|
||||||
});
|
"height": 0
|
||||||
if (instance) {
|
});
|
||||||
pluginInstance = instance;
|
if (instance) {
|
||||||
}
|
pluginInstance = instance;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
|
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
|
||||||
|
PluginService.reloadPlugin(pluginId);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
Qt.callLater(() => tryCreatePluginInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -970,6 +979,11 @@ Column {
|
|||||||
pluginInstance.loadPluginData();
|
pluginInstance.loadPluginData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onPluginLoaded(loadedPluginId) {
|
||||||
|
if (loadedPluginId !== pluginId || pluginInstance)
|
||||||
|
return;
|
||||||
|
Qt.callLater(() => tryCreatePluginInstance());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ Row {
|
|||||||
property Item popoutContent: null
|
property Item popoutContent: null
|
||||||
|
|
||||||
signal addWidget(string widgetId)
|
signal addWidget(string widgetId)
|
||||||
signal resetToDefault()
|
signal resetToDefault
|
||||||
signal clearAll()
|
signal clearAll
|
||||||
|
|
||||||
height: 48
|
height: 48
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
@@ -28,7 +28,7 @@ Row {
|
|||||||
y: parent ? Math.round((parent.height - height) / 2) : 0
|
y: parent ? Math.round((parent.height - height) / 2) : 0
|
||||||
width: 400
|
width: 400
|
||||||
height: 300
|
height: 300
|
||||||
modal: true
|
modal: false
|
||||||
focus: true
|
focus: true
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ Row {
|
|||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.addWidget(modelData.id)
|
root.addWidget(modelData.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ DankPopout {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
layerNamespace: "dms:control-center"
|
layerNamespace: "dms:control-center"
|
||||||
|
fullHeightSurface: true
|
||||||
|
|
||||||
property string expandedSection: ""
|
property string expandedSection: ""
|
||||||
property var triggerScreen: null
|
property var triggerScreen: null
|
||||||
@@ -115,11 +116,6 @@ DankPopout {
|
|||||||
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
property alias bluetoothCodecSelector: bluetoothCodecSelector
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 0
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -13,15 +13,12 @@ Rectangle {
|
|||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
if (height > 0) {
|
if (height > 0)
|
||||||
return height;
|
return height;
|
||||||
}
|
if (NetworkService.wifiToggling)
|
||||||
if (NetworkService.wifiToggling) {
|
|
||||||
return headerRow.height + wifiToggleContent.height + Theme.spacingM;
|
return headerRow.height + wifiToggleContent.height + Theme.spacingM;
|
||||||
}
|
if (NetworkService.wifiEnabled)
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
return headerRow.height + wifiContent.height + Theme.spacingM;
|
return headerRow.height + wifiContent.height + Theme.spacingM;
|
||||||
}
|
|
||||||
return headerRow.height + wifiOffContent.height + Theme.spacingM;
|
return headerRow.height + wifiOffContent.height + Theme.spacingM;
|
||||||
}
|
}
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
@@ -40,34 +37,40 @@ Rectangle {
|
|||||||
property bool hasEthernetAvailable: (NetworkService.ethernetDevices?.length ?? 0) > 0
|
property bool hasEthernetAvailable: (NetworkService.ethernetDevices?.length ?? 0) > 0
|
||||||
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
|
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
|
||||||
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
|
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
|
||||||
|
property int maxPinnedNetworks: 3
|
||||||
|
|
||||||
|
function normalizePinList(value) {
|
||||||
|
if (Array.isArray(value))
|
||||||
|
return value.filter(v => v);
|
||||||
|
if (typeof value === "string" && value.length > 0)
|
||||||
|
return [value];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPinnedNetworks() {
|
||||||
|
const pins = SettingsData.wifiNetworkPins || {};
|
||||||
|
return normalizePinList(pins["preferredWifi"]);
|
||||||
|
}
|
||||||
|
|
||||||
property int currentPreferenceIndex: {
|
property int currentPreferenceIndex: {
|
||||||
if (DMSService.apiVersion < 5) {
|
if (DMSService.apiVersion < 5)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10)
|
||||||
|
|
||||||
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
if (!hasEthernetAvailable)
|
||||||
|
|
||||||
if (!hasEthernetAvailable) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
if (!hasWifiAvailable)
|
||||||
|
|
||||||
if (!hasWifiAvailable) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
const pref = NetworkService.userPreference;
|
const pref = NetworkService.userPreference;
|
||||||
const status = NetworkService.networkStatus;
|
switch (pref) {
|
||||||
|
case "ethernet":
|
||||||
if (pref === "ethernet") {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
case "wifi":
|
||||||
if (pref === "wifi") {
|
|
||||||
return 1;
|
return 1;
|
||||||
|
default:
|
||||||
|
return NetworkService.networkStatus === "ethernet" ? 0 : 1;
|
||||||
}
|
}
|
||||||
return status === "ethernet" ? 0 : 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -78,7 +81,7 @@ Rectangle {
|
|||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingS
|
anchors.topMargin: Theme.spacingS
|
||||||
height: 40
|
height: Math.max(headerLeft.implicitHeight, rightControls.implicitHeight) + Theme.spacingS * 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: headerLeft
|
id: headerLeft
|
||||||
@@ -162,9 +165,10 @@ Rectangle {
|
|||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingM
|
anchors.topMargin: Theme.spacingM
|
||||||
visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
|
visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
|
||||||
height: visible ? 80 : 0
|
height: visible ? wifiToggleColumn.implicitHeight + Theme.spacingM * 2 : 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: wifiToggleColumn
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
@@ -201,9 +205,10 @@ Rectangle {
|
|||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingM
|
anchors.topMargin: Theme.spacingM
|
||||||
visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
||||||
height: visible ? 120 : 0
|
height: visible ? wifiOffColumn.implicitHeight + Theme.spacingM * 2 : 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
id: wifiOffColumn
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingL
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -226,14 +231,15 @@ Rectangle {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: 120
|
width: enableWifiLabel.implicitWidth + Theme.spacingL * 2
|
||||||
height: 36
|
height: enableWifiLabel.implicitHeight + Theme.spacingM * 2
|
||||||
radius: 18
|
radius: height / 2
|
||||||
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
border.color: Theme.primary
|
border.color: Theme.primary
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
id: enableWifiLabel
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: I18n.tr("Enable WiFi")
|
text: I18n.tr("Enable WiFi")
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
@@ -252,6 +258,25 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScriptModel {
|
||||||
|
id: wiredConnectionsModel
|
||||||
|
objectProp: "uuid"
|
||||||
|
values: {
|
||||||
|
const networks = NetworkService.wiredConnections;
|
||||||
|
if (!networks)
|
||||||
|
return [];
|
||||||
|
let sorted = [...networks];
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
if (a.isActive && !b.isActive)
|
||||||
|
return -1;
|
||||||
|
if (!a.isActive && b.isActive)
|
||||||
|
return 1;
|
||||||
|
return a.id.localeCompare(b.id);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
id: wiredContent
|
id: wiredContent
|
||||||
anchors.top: headerRow.bottom
|
anchors.top: headerRow.bottom
|
||||||
@@ -270,34 +295,25 @@ Rectangle {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ScriptModel {
|
model: wiredConnectionsModel
|
||||||
values: {
|
|
||||||
const currentUuid = NetworkService.ethernetConnectionUuid;
|
|
||||||
const networks = NetworkService.wiredConnections;
|
|
||||||
let sorted = [...networks];
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
if (a.isActive && !b.isActive)
|
|
||||||
return -1;
|
|
||||||
if (!a.isActive && b.isActive)
|
|
||||||
return 1;
|
|
||||||
return a.id.localeCompare(b.id);
|
|
||||||
});
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
|
id: wiredDelegate
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool isActive: modelData.isActive
|
||||||
|
readonly property string configName: modelData.id || I18n.tr("Unknown Config")
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
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 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
border.color: Theme.primary
|
border.color: Theme.primary
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
id: wiredContentRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
@@ -306,7 +322,7 @@ Rectangle {
|
|||||||
DankIcon {
|
DankIcon {
|
||||||
name: "lan"
|
name: "lan"
|
||||||
size: Theme.iconSize - 4
|
size: Theme.iconSize - 4
|
||||||
color: modelData.isActive ? Theme.primary : Theme.surfaceText
|
color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,10 +331,10 @@ Rectangle {
|
|||||||
width: 200
|
width: 200
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.id || I18n.tr("Unknown Config")
|
text: wiredDelegate.configName
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: modelData.isActive ? Theme.primary : Theme.surfaceText
|
color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
|
||||||
font.weight: modelData.isActive ? Font.Medium : Font.Normal
|
font.weight: wiredDelegate.isActive ? Font.Medium : Font.Normal
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
}
|
}
|
||||||
@@ -335,12 +351,12 @@ Rectangle {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
if (wiredNetworkContextMenu.visible) {
|
if (wiredNetworkContextMenu.visible) {
|
||||||
wiredNetworkContextMenu.close();
|
wiredNetworkContextMenu.close();
|
||||||
} else {
|
return;
|
||||||
wiredNetworkContextMenu.currentID = modelData.id;
|
|
||||||
wiredNetworkContextMenu.currentUUID = modelData.uuid;
|
|
||||||
wiredNetworkContextMenu.currentConnected = modelData.isActive;
|
|
||||||
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
|
|
||||||
}
|
}
|
||||||
|
wiredNetworkContextMenu.currentID = modelData.id;
|
||||||
|
wiredNetworkContextMenu.currentUUID = modelData.uuid;
|
||||||
|
wiredNetworkContextMenu.currentConnected = wiredDelegate.isActive;
|
||||||
|
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,9 +373,8 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => wiredRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => wiredRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: function (event) {
|
onClicked: function (event) {
|
||||||
if (modelData.uuid !== NetworkService.ethernetConnectionUuid) {
|
if (modelData.uuid !== NetworkService.ethernetConnectionUuid)
|
||||||
NetworkService.connectToSpecificWiredConfig(modelData.uuid);
|
NetworkService.connectToSpecificWiredConfig(modelData.uuid);
|
||||||
}
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,9 +418,8 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!wiredNetworkContextMenu.currentConnected) {
|
if (!wiredNetworkContextMenu.currentConnected)
|
||||||
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID);
|
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,13 +465,46 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
|
const networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
|
||||||
networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
|
networkWiredInfoModalLoader.active = true;
|
||||||
|
networkWiredInfoModalLoader.item.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankFlickable {
|
ScriptModel {
|
||||||
|
id: wifiNetworksModel
|
||||||
|
objectProp: "ssid"
|
||||||
|
values: wifiContent.menuOpen ? wifiContent.frozenNetworks : wifiContent.sortedNetworks
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: wifiScanningOverlay
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && NetworkService.wifiInterface && (NetworkService.wifiNetworks?.length ?? 0) < 1 && NetworkService.isScanning
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "refresh"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: wifiScanningOverlay.visible
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankListView {
|
||||||
id: wifiContent
|
id: wifiContent
|
||||||
anchors.top: headerRow.bottom
|
anchors.top: headerRow.bottom
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -465,31 +512,17 @@ Rectangle {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
anchors.topMargin: Theme.spacingM
|
anchors.topMargin: Theme.spacingM
|
||||||
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling
|
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && !wifiScanningOverlay.visible
|
||||||
contentHeight: wifiColumn.height
|
|
||||||
clip: true
|
clip: true
|
||||||
|
spacing: Theme.spacingS
|
||||||
property int maxPinnedNetworks: 3
|
model: wifiNetworksModel
|
||||||
|
|
||||||
function normalizePinList(value) {
|
|
||||||
if (Array.isArray(value))
|
|
||||||
return value.filter(v => v);
|
|
||||||
if (typeof value === "string" && value.length > 0)
|
|
||||||
return [value];
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPinnedNetworks() {
|
|
||||||
const pins = SettingsData.wifiNetworkPins || {};
|
|
||||||
return normalizePinList(pins["preferredWifi"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
property var frozenNetworks: []
|
property var frozenNetworks: []
|
||||||
property bool menuOpen: false
|
property bool menuOpen: false
|
||||||
property var sortedNetworks: {
|
property var sortedNetworks: {
|
||||||
const ssid = NetworkService.currentWifiSSID;
|
const ssid = NetworkService.currentWifiSSID;
|
||||||
const networks = NetworkService.wifiNetworks;
|
const networks = NetworkService.wifiNetworks;
|
||||||
const pinnedList = getPinnedNetworks();
|
const pinnedList = root.getPinnedNetworks();
|
||||||
|
|
||||||
let sorted = [...networks];
|
let sorted = [...networks];
|
||||||
sorted.sort((a, b) => {
|
sorted.sort((a, b) => {
|
||||||
@@ -519,229 +552,188 @@ Rectangle {
|
|||||||
frozenNetworks = sortedNetworks;
|
frozenNetworks = sortedNetworks;
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
delegate: Rectangle {
|
||||||
id: wifiColumn
|
id: wifiDelegate
|
||||||
width: parent.width
|
required property var modelData
|
||||||
spacing: Theme.spacingS
|
required property int index
|
||||||
|
|
||||||
Item {
|
readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
|
||||||
width: parent.width
|
readonly property bool isPinned: root.getPinnedNetworks().includes(modelData.ssid)
|
||||||
height: 200
|
readonly property string networkName: modelData.ssid || I18n.tr("Unknown Network")
|
||||||
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling && NetworkService.isScanning
|
readonly property int signalStrength: modelData.signal || 0
|
||||||
|
|
||||||
|
width: wifiContent.width
|
||||||
|
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
|
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: wifiContentRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
name: {
|
||||||
name: "refresh"
|
if (wifiDelegate.signalStrength >= 50)
|
||||||
size: 48
|
return "wifi";
|
||||||
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
if (wifiDelegate.signalStrength >= 25)
|
||||||
|
return "wifi_2_bar";
|
||||||
|
return "wifi_1_bar";
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: wifiDelegate.isConnected ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
Column {
|
||||||
running: NetworkService.isScanning
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
loops: Animation.Infinite
|
width: 200
|
||||||
from: 0
|
|
||||||
to: 360
|
StyledText {
|
||||||
duration: 1000
|
text: wifiDelegate.networkName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: wifiDelegate.isConnected ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: wifiDelegate.isConnected ? I18n.tr("Connected") + " \u2022" : (modelData.secured ? I18n.tr("Secured") + " \u2022" : I18n.tr("Open") + " \u2022")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.saved ? I18n.tr("Saved") : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: (modelData.saved ? "\u2022 " : "") + wifiDelegate.signalStrength + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
DankActionButton {
|
||||||
model: ScriptModel {
|
id: optionsButton
|
||||||
values: wifiContent.menuOpen ? wifiContent.frozenNetworks : wifiContent.sortedNetworks
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (networkContextMenu.visible) {
|
||||||
|
networkContextMenu.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wifiContent.menuOpen = true;
|
||||||
|
networkContextMenu.currentSSID = modelData.ssid;
|
||||||
|
networkContextMenu.currentSecured = modelData.secured;
|
||||||
|
networkContextMenu.currentConnected = wifiDelegate.isConnected;
|
||||||
|
networkContextMenu.currentSaved = modelData.saved;
|
||||||
|
networkContextMenu.currentSignal = modelData.signal;
|
||||||
|
networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
|
||||||
|
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: pinWifiRow.width + Theme.spacingS * 2
|
||||||
|
height: pinWifiRow.implicitHeight + Theme.spacingXS * 2
|
||||||
|
radius: height / 2
|
||||||
|
color: wifiDelegate.isPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: pinWifiRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "push_pin"
|
||||||
|
size: 16
|
||||||
|
color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: wifiDelegate.isPinned ? I18n.tr("Pinned") : I18n.tr("Pin")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
DankRipple {
|
||||||
required property var modelData
|
id: pinRipple
|
||||||
required property int index
|
cornerRadius: parent.radius
|
||||||
|
}
|
||||||
|
|
||||||
width: parent.width
|
MouseArea {
|
||||||
height: 50
|
anchors.fill: parent
|
||||||
radius: Theme.cornerRadius
|
cursorShape: Qt.PointingHandCursor
|
||||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
||||||
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
onClicked: {
|
||||||
border.width: 0
|
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
||||||
|
let pinnedList = root.normalizePinList(pins["preferredWifi"]);
|
||||||
|
const pinIndex = pinnedList.indexOf(modelData.ssid);
|
||||||
|
|
||||||
Row {
|
if (pinIndex !== -1) {
|
||||||
anchors.left: parent.left
|
pinnedList.splice(pinIndex, 1);
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
} else {
|
||||||
anchors.leftMargin: Theme.spacingM
|
pinnedList.unshift(modelData.ssid);
|
||||||
spacing: Theme.spacingS
|
if (pinnedList.length > root.maxPinnedNetworks)
|
||||||
|
pinnedList = pinnedList.slice(0, root.maxPinnedNetworks);
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
let strength = modelData.signal || 0;
|
|
||||||
if (strength >= 50)
|
|
||||||
return "wifi";
|
|
||||||
if (strength >= 25)
|
|
||||||
return "wifi_2_bar";
|
|
||||||
return "wifi_1_bar";
|
|
||||||
}
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
if (pinnedList.length > 0)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
pins["preferredWifi"] = pinnedList;
|
||||||
width: 200
|
else
|
||||||
|
delete pins["preferredWifi"];
|
||||||
|
|
||||||
StyledText {
|
SettingsData.set("wifiNetworkPins", pins);
|
||||||
text: modelData.ssid || I18n.tr("Unknown Network")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.ssid === NetworkService.currentWifiSSID ? I18n.tr("Connected") + " •" : (modelData.secured ? I18n.tr("Secured") + " •" : I18n.tr("Open") + " •")
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.saved ? I18n.tr("Saved") : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (modelData.saved ? "• " : "") + modelData.signal + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankRipple {
|
||||||
id: optionsButton
|
id: wifiRipple
|
||||||
anchors.right: parent.right
|
cornerRadius: parent.radius
|
||||||
anchors.rightMargin: Theme.spacingS
|
}
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
iconName: "more_horiz"
|
MouseArea {
|
||||||
buttonSize: 28
|
id: networkMouseArea
|
||||||
onClicked: {
|
anchors.fill: parent
|
||||||
if (networkContextMenu.visible) {
|
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4
|
||||||
networkContextMenu.close();
|
hoverEnabled: true
|
||||||
} else {
|
cursorShape: Qt.PointingHandCursor
|
||||||
wifiContent.menuOpen = true;
|
onPressed: mouse => wifiRipple.trigger(mouse.x, mouse.y)
|
||||||
networkContextMenu.currentSSID = modelData.ssid;
|
onClicked: function (event) {
|
||||||
networkContextMenu.currentSecured = modelData.secured;
|
if (wifiDelegate.isConnected) {
|
||||||
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID;
|
event.accepted = true;
|
||||||
networkContextMenu.currentSaved = modelData.saved;
|
return;
|
||||||
networkContextMenu.currentSignal = modelData.signal;
|
|
||||||
networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
|
|
||||||
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (modelData.secured && !modelData.saved && DMSService.apiVersion < 7) {
|
||||||
Rectangle {
|
PopoutService.showWifiPasswordModal(modelData.ssid);
|
||||||
anchors.right: parent.right
|
} else {
|
||||||
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS
|
NetworkService.connectToWifi(modelData.ssid);
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: pinWifiRow.width + Theme.spacingS * 2
|
|
||||||
height: 28
|
|
||||||
radius: height / 2
|
|
||||||
color: {
|
|
||||||
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
|
||||||
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: pinWifiRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "push_pin"
|
|
||||||
size: 16
|
|
||||||
color: {
|
|
||||||
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
|
||||||
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
|
||||||
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: {
|
|
||||||
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
|
|
||||||
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankRipple {
|
|
||||||
id: pinRipple
|
|
||||||
cornerRadius: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
|
||||||
onClicked: {
|
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
|
|
||||||
let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"]);
|
|
||||||
const pinIndex = pinnedList.indexOf(modelData.ssid);
|
|
||||||
|
|
||||||
if (pinIndex !== -1) {
|
|
||||||
pinnedList.splice(pinIndex, 1);
|
|
||||||
} else {
|
|
||||||
pinnedList.unshift(modelData.ssid);
|
|
||||||
if (pinnedList.length > wifiContent.maxPinnedNetworks)
|
|
||||||
pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinnedList.length > 0)
|
|
||||||
pins["preferredWifi"] = pinnedList;
|
|
||||||
else
|
|
||||||
delete pins["preferredWifi"];
|
|
||||||
|
|
||||||
SettingsData.set("wifiNetworkPins", pins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankRipple {
|
|
||||||
id: wifiRipple
|
|
||||||
cornerRadius: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onPressed: mouse => wifiRipple.trigger(mouse.x, mouse.y)
|
|
||||||
onClicked: function (event) {
|
|
||||||
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
|
||||||
if (modelData.secured && !modelData.saved) {
|
|
||||||
if (DMSService.apiVersion >= 7) {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid);
|
|
||||||
} else {
|
|
||||||
PopoutService.showWifiPasswordModal(modelData.ssid);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -759,6 +751,8 @@ Rectangle {
|
|||||||
property int currentSignal: 0
|
property int currentSignal: 0
|
||||||
property bool currentAutoconnect: false
|
property bool currentAutoconnect: false
|
||||||
|
|
||||||
|
readonly property bool showSavedOptions: currentSaved || currentConnected
|
||||||
|
|
||||||
onClosed: {
|
onClosed: {
|
||||||
wifiContent.menuOpen = false;
|
wifiContent.menuOpen = false;
|
||||||
}
|
}
|
||||||
@@ -790,17 +784,13 @@ Rectangle {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (networkContextMenu.currentConnected) {
|
if (networkContextMenu.currentConnected) {
|
||||||
NetworkService.disconnectWifi();
|
NetworkService.disconnectWifi();
|
||||||
} else {
|
return;
|
||||||
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
|
||||||
if (DMSService.apiVersion >= 7) {
|
|
||||||
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
|
||||||
} else {
|
|
||||||
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved && DMSService.apiVersion < 7) {
|
||||||
|
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NetworkService.connectToWifi(networkContextMenu.currentSSID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,15 +812,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
|
const networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
|
||||||
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData);
|
networkInfoModalLoader.active = true;
|
||||||
|
networkInfoModalLoader.item.showNetworkInfo(networkContextMenu.currentSSID, networkData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: networkContextMenu.currentAutoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
|
text: networkContextMenu.currentAutoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
|
||||||
height: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13 ? 32 : 0
|
height: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13 ? 32 : 0
|
||||||
visible: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13
|
visible: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13
|
||||||
|
|
||||||
contentItem: StyledText {
|
contentItem: StyledText {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
@@ -852,8 +843,8 @@ Rectangle {
|
|||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: I18n.tr("Forget Network")
|
text: I18n.tr("Forget Network")
|
||||||
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
height: networkContextMenu.showSavedOptions ? 32 : 0
|
||||||
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
visible: networkContextMenu.showSavedOptions
|
||||||
|
|
||||||
contentItem: StyledText {
|
contentItem: StyledText {
|
||||||
text: parent.text
|
text: parent.text
|
||||||
@@ -874,11 +865,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkInfoModal {
|
Loader {
|
||||||
id: networkInfoModal
|
id: networkInfoModalLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: NetworkInfoModal {}
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkWiredInfoModal {
|
Loader {
|
||||||
id: networkWiredInfoModal
|
id: networkWiredInfoModalLoader
|
||||||
|
active: false
|
||||||
|
sourceComponent: NetworkWiredInfoModal {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,14 +216,18 @@ QtObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
|
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
|
||||||
if (!pluginComponent || typeof pluginComponent.createObject !== 'function') {
|
if (!pluginComponent)
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
const tempInstance = pluginComponent.createObject(null);
|
let tempInstance;
|
||||||
if (!tempInstance) {
|
try {
|
||||||
|
tempInstance = pluginComponent.createObject(null);
|
||||||
|
} catch (e) {
|
||||||
|
PluginService.reloadPlugin(plugin.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!tempInstance)
|
||||||
|
continue;
|
||||||
|
|
||||||
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
|
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
|
||||||
tempInstance.destroy();
|
tempInstance.destroy();
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Item {
|
|||||||
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
||||||
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||||
focusedScreenName = NiriService.currentOutput;
|
focusedScreenName = NiriService.currentOutput;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||||
@@ -125,7 +125,7 @@ Item {
|
|||||||
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
|
||||||
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
} else if (CompositorService.isNiri && NiriService.currentOutput) {
|
||||||
focusedScreenName = NiriService.currentOutput;
|
focusedScreenName = NiriService.currentOutput;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ Item {
|
|||||||
}, (_, i) => i);
|
}, (_, i) => i);
|
||||||
}
|
}
|
||||||
return DwlService.getVisibleTags(barWindow.screenName);
|
return DwlService.getVisibleTags(barWindow.screenName);
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const workspaces = I3.workspaces?.values || [];
|
const workspaces = I3.workspaces?.values || [];
|
||||||
if (workspaces.length === 0)
|
if (workspaces.length === 0)
|
||||||
return [
|
return [
|
||||||
@@ -145,7 +145,7 @@ Item {
|
|||||||
return 0;
|
return 0;
|
||||||
const activeTags = DwlService.getActiveTags(barWindow.screenName);
|
const activeTags = DwlService.getActiveTags(barWindow.screenName);
|
||||||
return activeTags.length > 0 ? activeTags[0] : 0;
|
return activeTags.length > 0 ? activeTags[0] : 0;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
|
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||||
return focusedWs ? focusedWs.num : 1;
|
return focusedWs ? focusedWs.num : 1;
|
||||||
@@ -194,7 +194,7 @@ Item {
|
|||||||
if (nextIndex !== validIndex) {
|
if (nextIndex !== validIndex) {
|
||||||
DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]);
|
DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]);
|
||||||
}
|
}
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const currentWs = getCurrentWorkspace();
|
const currentWs = getCurrentWorkspace();
|
||||||
const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs);
|
const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs);
|
||||||
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
const validIndex = currentIndex === -1 ? 0 : currentIndex;
|
||||||
|
|||||||
@@ -48,11 +48,6 @@ DankPopout {
|
|||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
focus: true
|
focus: true
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (root.shouldBeVisible) {
|
if (root.shouldBeVisible) {
|
||||||
@@ -78,35 +73,6 @@ DankPopout {
|
|||||||
target: root
|
target: root
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -3
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 3
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.05)
|
|
||||||
border.width: 0
|
|
||||||
z: -3
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -2
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 2
|
|
||||||
border.color: Theme.shadowMedium
|
|
||||||
border.width: 0
|
|
||||||
z: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 0
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
|
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ DankPopout {
|
|||||||
|
|
||||||
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
antialiasing: true
|
|
||||||
smooth: true
|
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -142,15 +137,6 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Theme.outlineStrong
|
|
||||||
border.width: 0
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ DankPopout {
|
|||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
implicitHeight: contentColumn.height + Theme.spacingL * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ BasePill {
|
|||||||
Component.onCompleted: updateModel()
|
Component.onCompleted: updateModel()
|
||||||
|
|
||||||
visible: dockItems.length > 0
|
visible: dockItems.length > 0
|
||||||
|
readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
|
||||||
|
|
||||||
content: Component {
|
content: Component {
|
||||||
Item {
|
Item {
|
||||||
@@ -460,7 +461,7 @@ BasePill {
|
|||||||
readonly property bool isOverflowToggle: modelData.type === "overflow-toggle"
|
readonly property bool isOverflowToggle: modelData.type === "overflow-toggle"
|
||||||
readonly property bool isInOverflow: modelData.isInOverflow === true
|
readonly property bool isInOverflow: modelData.isInOverflow === true
|
||||||
|
|
||||||
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120))
|
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120))
|
||||||
readonly property real visualWidth: root.isVerticalOrientation ? root.barThickness : visualSize
|
readonly property real visualWidth: root.isVerticalOrientation ? root.barThickness : visualSize
|
||||||
readonly property real visualHeight: root.isVerticalOrientation ? visualSize : root.barThickness
|
readonly property real visualHeight: root.isVerticalOrientation ? visualSize : root.barThickness
|
||||||
|
|
||||||
@@ -620,8 +621,8 @@ BasePill {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: visualContent
|
id: visualContent
|
||||||
width: root.isVerticalOrientation ? 24 : delegateItem.visualSize
|
width: root.isVerticalOrientation ? root.iconCellSize : delegateItem.visualSize
|
||||||
height: root.isVerticalOrientation ? delegateItem.visualSize : 24
|
height: root.isVerticalOrientation ? delegateItem.visualSize : root.iconCellSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
@@ -937,18 +938,15 @@ BasePill {
|
|||||||
const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2);
|
const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2);
|
||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
||||||
const barThickness = root.effectiveBarThickness;
|
|
||||||
const spacing = barConfig?.spacing ?? 4;
|
|
||||||
const isLeft = root.axis?.edge === "left";
|
const isLeft = root.axis?.edge === "left";
|
||||||
const tooltipOffset = barThickness + spacing + Theme.spacingM;
|
const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
|
||||||
const tooltipX = isLeft ? tooltipOffset : (root.parentScreen.width - tooltipOffset);
|
const screenRelativeY = globalPos.y - screenY + root.minTooltipY;
|
||||||
const screenRelativeY = globalPos.y - screenY + root.barY;
|
|
||||||
tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft);
|
tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft);
|
||||||
} else {
|
} else {
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
|
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
|
||||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
|
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
|
||||||
const isBottom = root.axis?.edge === "bottom";
|
const isBottom = root.axis?.edge === "bottom";
|
||||||
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
|
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
|
||||||
tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
|
tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -972,21 +970,27 @@ BasePill {
|
|||||||
|
|
||||||
if (contextMenuLoader.item) {
|
if (contextMenuLoader.item) {
|
||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
|
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
|
||||||
|
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
||||||
|
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
||||||
const isBarVertical = root.axis?.isVertical ?? false;
|
const isBarVertical = root.axis?.isVertical ?? false;
|
||||||
const barEdge = root.axis?.edge ?? "top";
|
const barEdge = root.axis?.edge ?? "top";
|
||||||
|
|
||||||
let x = globalPos.x;
|
let x = globalPos.x - screenX;
|
||||||
let y = globalPos.y;
|
let y = globalPos.y - screenY;
|
||||||
|
|
||||||
if (barEdge === "bottom") {
|
switch (barEdge) {
|
||||||
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.effectiveBarThickness;
|
case "bottom":
|
||||||
} else if (barEdge === "top") {
|
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.barThickness - root.barSpacing;
|
||||||
y = root.effectiveBarThickness;
|
break;
|
||||||
} else if (barEdge === "left") {
|
case "top":
|
||||||
x = root.effectiveBarThickness;
|
y = root.barThickness + root.barSpacing;
|
||||||
} else if (barEdge === "right") {
|
break;
|
||||||
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.effectiveBarThickness;
|
case "left":
|
||||||
|
x = root.barThickness + root.barSpacing;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.barThickness - root.barSpacing;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldHidePin = modelData.appId === "org.quickshell";
|
const shouldHidePin = modelData.appId === "org.quickshell";
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc)
|
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||||
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
|
||||||
@@ -72,6 +72,8 @@ BasePill {
|
|||||||
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||||
} else if (CompositorService.isScroll) {
|
} else if (CompositorService.isScroll) {
|
||||||
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
return "file://" + Theme.shellDir + "/assets/sway.svg";
|
||||||
|
} else if (CompositorService.isMiracle) {
|
||||||
|
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
|
||||||
} else if (CompositorService.isLabwc) {
|
} else if (CompositorService.isLabwc) {
|
||||||
return "file://" + Theme.shellDir + "/assets/labwc.png";
|
return "file://" + Theme.shellDir + "/assets/labwc.png";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,12 +135,27 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
|
||||||
|
readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
|
||||||
|
|
||||||
|
readonly property string focusedAppId: {
|
||||||
|
const toplevels = CompositorService.sortedToplevels;
|
||||||
|
if (!toplevels)
|
||||||
|
return "";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < toplevels.length; i++) {
|
||||||
|
if (toplevels[i].activated)
|
||||||
|
result = toplevels[i].appId || "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
visible: windowCount > 0
|
visible: windowCount > 0
|
||||||
|
|
||||||
property real scrollAccumulator: 0
|
property real scrollAccumulator: 0
|
||||||
property real touchpadThreshold: 500
|
property real touchpadThreshold: 500
|
||||||
|
|
||||||
onWheel: function (wheelEvent) {
|
onWheel: function (wheelEvent) {
|
||||||
|
wheelEvent.accepted = true;
|
||||||
const deltaY = wheelEvent.angleDelta.y;
|
const deltaY = wheelEvent.angleDelta.y;
|
||||||
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||||
|
|
||||||
@@ -226,7 +241,7 @@ BasePill {
|
|||||||
property bool isGrouped: root._groupByApp
|
property bool isGrouped: root._groupByApp
|
||||||
property var groupData: isGrouped ? modelData : null
|
property var groupData: isGrouped ? modelData : null
|
||||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
|
||||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||||
property var toplevelObject: toplevelData
|
property var toplevelObject: toplevelData
|
||||||
@@ -242,7 +257,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "");
|
return appName + (windowTitle ? " • " + windowTitle : "");
|
||||||
}
|
}
|
||||||
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120)
|
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
|
||||||
|
|
||||||
width: visualWidth
|
width: visualWidth
|
||||||
height: root.barThickness
|
height: root.barThickness
|
||||||
@@ -250,7 +265,7 @@ BasePill {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: visualContent
|
id: visualContent
|
||||||
width: delegateItem.visualWidth
|
width: delegateItem.visualWidth
|
||||||
height: 24
|
height: root.iconCellSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
@@ -433,7 +448,7 @@ BasePill {
|
|||||||
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
const screenX = root.parentScreen ? root.parentScreen.x : 0;
|
||||||
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
const screenY = root.parentScreen ? root.parentScreen.y : 0;
|
||||||
const relativeY = globalPos.y - screenY;
|
const relativeY = globalPos.y - screenY;
|
||||||
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
|
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
|
||||||
const isLeft = root.axis?.edge === "left";
|
const isLeft = root.axis?.edge === "left";
|
||||||
const adjustedY = relativeY + root.minTooltipY;
|
const adjustedY = relativeY + root.minTooltipY;
|
||||||
const finalX = screenX + tooltipX;
|
const finalX = screenX + tooltipX;
|
||||||
@@ -442,7 +457,7 @@ BasePill {
|
|||||||
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
|
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
|
||||||
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
|
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
|
||||||
const isBottom = root.axis?.edge === "bottom";
|
const isBottom = root.axis?.edge === "bottom";
|
||||||
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
|
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
|
||||||
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
|
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,7 +496,7 @@ BasePill {
|
|||||||
property bool isGrouped: root._groupByApp
|
property bool isGrouped: root._groupByApp
|
||||||
property var groupData: isGrouped ? modelData : null
|
property var groupData: isGrouped ? modelData : null
|
||||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||||
property bool isFocused: toplevelData ? toplevelData.activated : false
|
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
|
||||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||||
property var toplevelObject: toplevelData
|
property var toplevelObject: toplevelData
|
||||||
@@ -497,15 +512,15 @@ BasePill {
|
|||||||
}
|
}
|
||||||
return appName + (windowTitle ? " • " + windowTitle : "");
|
return appName + (windowTitle ? " • " + windowTitle : "");
|
||||||
}
|
}
|
||||||
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120)
|
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
|
||||||
|
|
||||||
width: root.barThickness
|
width: root.barThickness
|
||||||
height: 24
|
height: root.iconCellSize
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: visualContent
|
id: visualContent
|
||||||
width: delegateItem.visualWidth
|
width: delegateItem.visualWidth
|
||||||
height: 24
|
height: root.iconCellSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: {
|
color: {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ BasePill {
|
|||||||
property bool suppressShiftAnimation: false
|
property bool suppressShiftAnimation: false
|
||||||
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
||||||
visible: allTrayItems.length > 0
|
visible: allTrayItems.length > 0
|
||||||
|
readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
|
||||||
|
|
||||||
readonly property real minTooltipY: {
|
readonly property real minTooltipY: {
|
||||||
if (!parentScreen || !isVerticalOrientation) {
|
if (!parentScreen || !isVerticalOrientation) {
|
||||||
@@ -172,7 +173,7 @@ BasePill {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: root.barThickness
|
height: root.barThickness
|
||||||
z: dragHandler.dragging ? 100 : 0
|
z: dragHandler.dragging ? 100 : 0
|
||||||
|
|
||||||
@@ -183,7 +184,7 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
const dragIdx = root.draggedIndex;
|
const dragIdx = root.draggedIndex;
|
||||||
const dropIdx = root.dropTargetIndex;
|
const dropIdx = root.dropTargetIndex;
|
||||||
const shiftAmount = 24;
|
const shiftAmount = root.trayItemSize;
|
||||||
if (dropIdx < 0)
|
if (dropIdx < 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||||
@@ -222,8 +223,8 @@ BasePill {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: visualContent
|
id: visualContent
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||||
@@ -328,7 +329,7 @@ BasePill {
|
|||||||
|
|
||||||
const axisOffset = mouse.x - dragHandler.dragStartPos.x;
|
const axisOffset = mouse.x - dragHandler.dragStartPos.x;
|
||||||
dragHandler.dragAxisOffset = axisOffset;
|
dragHandler.dragAxisOffset = axisOffset;
|
||||||
const itemSize = 24;
|
const itemSize = root.trayItemSize;
|
||||||
const slotOffset = Math.round(axisOffset / itemSize);
|
const slotOffset = Math.round(axisOffset / itemSize);
|
||||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||||
if (newTargetIndex !== root.dropTargetIndex) {
|
if (newTargetIndex !== root.dropTargetIndex) {
|
||||||
@@ -351,14 +352,14 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: root.barThickness
|
height: root.barThickness
|
||||||
visible: root.hasHiddenItems
|
visible: root.hasHiddenItems
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: caretButton
|
id: caretButton
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||||
@@ -430,7 +431,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
width: root.barThickness
|
width: root.barThickness
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
z: dragHandler.dragging ? 100 : 0
|
z: dragHandler.dragging ? 100 : 0
|
||||||
|
|
||||||
property real shiftOffset: {
|
property real shiftOffset: {
|
||||||
@@ -440,7 +441,7 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
const dragIdx = root.draggedIndex;
|
const dragIdx = root.draggedIndex;
|
||||||
const dropIdx = root.dropTargetIndex;
|
const dropIdx = root.dropTargetIndex;
|
||||||
const shiftAmount = 24;
|
const shiftAmount = root.trayItemSize;
|
||||||
if (dropIdx < 0)
|
if (dropIdx < 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||||
@@ -479,8 +480,8 @@ BasePill {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: visualContent
|
id: visualContent
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||||
@@ -585,7 +586,7 @@ BasePill {
|
|||||||
|
|
||||||
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
||||||
dragHandler.dragAxisOffset = axisOffset;
|
dragHandler.dragAxisOffset = axisOffset;
|
||||||
const itemSize = 24;
|
const itemSize = root.trayItemSize;
|
||||||
const slotOffset = Math.round(axisOffset / itemSize);
|
const slotOffset = Math.round(axisOffset / itemSize);
|
||||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||||
if (newTargetIndex !== root.dropTargetIndex) {
|
if (newTargetIndex !== root.dropTargetIndex) {
|
||||||
@@ -609,13 +610,13 @@ BasePill {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: root.barThickness
|
width: root.barThickness
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
visible: root.hasHiddenItems
|
visible: root.hasHiddenItems
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: caretButtonVert
|
id: caretButtonVert
|
||||||
width: 24
|
width: root.trayItemSize
|
||||||
height: 24
|
height: root.trayItemSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
|
||||||
@@ -835,7 +836,7 @@ BasePill {
|
|||||||
readonly property real rawWidth: {
|
readonly property real rawWidth: {
|
||||||
const itemCount = root.hiddenBarItems.length;
|
const itemCount = root.hiddenBarItems.length;
|
||||||
const cols = Math.min(5, itemCount);
|
const cols = Math.min(5, itemCount);
|
||||||
const itemSize = 28;
|
const itemSize = root.trayItemSize + 4;
|
||||||
const spacing = 2;
|
const spacing = 2;
|
||||||
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
|
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
|
||||||
}
|
}
|
||||||
@@ -843,7 +844,7 @@ BasePill {
|
|||||||
const itemCount = root.hiddenBarItems.length;
|
const itemCount = root.hiddenBarItems.length;
|
||||||
const cols = Math.min(5, itemCount);
|
const cols = Math.min(5, itemCount);
|
||||||
const rows = Math.ceil(itemCount / cols);
|
const rows = Math.ceil(itemCount / cols);
|
||||||
const itemSize = 28;
|
const itemSize = root.trayItemSize + 4;
|
||||||
const spacing = 2;
|
const spacing = 2;
|
||||||
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
|
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
|
||||||
}
|
}
|
||||||
@@ -982,8 +983,8 @@ BasePill {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
width: 28
|
width: root.trayItemSize + 4
|
||||||
height: 28
|
height: root.trayItemSize + 4
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,45 @@ BasePill {
|
|||||||
property bool isActive: false
|
property bool isActive: false
|
||||||
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
|
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
|
||||||
readonly property bool isChecking: SystemUpdateService.isChecking
|
readonly property bool isChecking: SystemUpdateService.isChecking
|
||||||
|
readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError
|
||||||
|
|
||||||
|
opacity: shouldHide ? 0 : 1
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "hidden_horizontal"
|
||||||
|
when: root.shouldHide && !isVerticalOrientation
|
||||||
|
PropertyChanges {
|
||||||
|
target: root
|
||||||
|
width: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "hidden_vertical"
|
||||||
|
when: root.shouldHide && isVerticalOrientation
|
||||||
|
PropertyChanges {
|
||||||
|
target: root
|
||||||
|
height: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
properties: "width,height"
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ref {
|
Ref {
|
||||||
service: SystemUpdateService
|
service: SystemUpdateService
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Item {
|
|||||||
return DwlService.activeOutput || root.screenName;
|
return DwlService.activeOutput || root.screenName;
|
||||||
case "sway":
|
case "sway":
|
||||||
case "scroll":
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||||
return focusedWs?.monitor?.name || root.screenName;
|
return focusedWs?.monitor?.name || root.screenName;
|
||||||
default:
|
default:
|
||||||
@@ -44,7 +45,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && ExtWorkspaceService.extWorkspaceAvailable)
|
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable)
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: DesktopEntries
|
target: DesktopEntries
|
||||||
@@ -67,6 +68,7 @@ Item {
|
|||||||
return activeTags.length > 0 ? activeTags[0] : -1;
|
return activeTags.length > 0 ? activeTags[0] : -1;
|
||||||
case "sway":
|
case "sway":
|
||||||
case "scroll":
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
return getSwayActiveWorkspace();
|
return getSwayActiveWorkspace();
|
||||||
default:
|
default:
|
||||||
return 1;
|
return 1;
|
||||||
@@ -97,6 +99,7 @@ Item {
|
|||||||
break;
|
break;
|
||||||
case "sway":
|
case "sway":
|
||||||
case "scroll":
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
baseList = getSwayWorkspaces();
|
baseList = getSwayWorkspaces();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -114,12 +117,23 @@ Item {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function mapWorkspace(ws) {
|
||||||
|
return {
|
||||||
|
"num": ws.number,
|
||||||
|
"name": ws.name,
|
||||||
|
"focused": ws.focused,
|
||||||
|
"active": ws.active,
|
||||||
|
"urgent": ws.urgent,
|
||||||
|
"monitor": ws.monitor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.screenName || SettingsData.workspaceFollowFocus) {
|
if (!root.screenName || SettingsData.workspaceFollowFocus) {
|
||||||
return workspaces.slice().sort((a, b) => a.num - b.num);
|
return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName);
|
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName);
|
||||||
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [
|
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [
|
||||||
{
|
{
|
||||||
"num": 1
|
"num": 1
|
||||||
}
|
}
|
||||||
@@ -222,7 +236,7 @@ Item {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
targetWorkspaceId = ws.tag;
|
targetWorkspaceId = ws.tag;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
|
targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
@@ -234,7 +248,7 @@ Item {
|
|||||||
let isActiveWs = false;
|
let isActiveWs = false;
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active);
|
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active);
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
|
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
|
||||||
} else if (CompositorService.isDwl) {
|
} else if (CompositorService.isDwl) {
|
||||||
@@ -255,7 +269,7 @@ Item {
|
|||||||
let winWs = null;
|
let winWs = null;
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
winWs = w.workspace_id;
|
winWs = w.workspace_id;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
winWs = w.workspace?.num;
|
winWs = w.workspace?.num;
|
||||||
} else {
|
} else {
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
|
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
|
||||||
@@ -322,7 +336,7 @@ Item {
|
|||||||
placeholder = {
|
placeholder = {
|
||||||
"tag": -1
|
"tag": -1
|
||||||
};
|
};
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
placeholder = {
|
placeholder = {
|
||||||
"num": -1
|
"num": -1
|
||||||
};
|
};
|
||||||
@@ -516,7 +530,7 @@ Item {
|
|||||||
return ws && ws.id !== -1;
|
return ws && ws.id !== -1;
|
||||||
if (CompositorService.isDwl)
|
if (CompositorService.isDwl)
|
||||||
return ws && ws.tag !== -1;
|
return ws && ws.tag !== -1;
|
||||||
if (CompositorService.isSway || CompositorService.isScroll)
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
return ws && ws.num !== -1;
|
return ws && ws.num !== -1;
|
||||||
return ws !== -1;
|
return ws !== -1;
|
||||||
});
|
});
|
||||||
@@ -588,7 +602,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
const realWorkspaces = getRealWorkspaces();
|
const realWorkspaces = getRealWorkspaces();
|
||||||
if (realWorkspaces.length < 2) {
|
if (realWorkspaces.length < 2) {
|
||||||
return;
|
return;
|
||||||
@@ -617,7 +631,7 @@ Item {
|
|||||||
return modelData?.id || "";
|
return modelData?.id || "";
|
||||||
if (CompositorService.isDwl)
|
if (CompositorService.isDwl)
|
||||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
||||||
if (CompositorService.isSway || CompositorService.isScroll)
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
return modelData?.num || "";
|
return modelData?.num || "";
|
||||||
return modelData - 1;
|
return modelData - 1;
|
||||||
}
|
}
|
||||||
@@ -632,7 +646,7 @@ Item {
|
|||||||
isPlaceholder = modelData?.id === -1;
|
isPlaceholder = modelData?.id === -1;
|
||||||
} else if (CompositorService.isDwl) {
|
} else if (CompositorService.isDwl) {
|
||||||
isPlaceholder = modelData?.tag === -1;
|
isPlaceholder = modelData?.tag === -1;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
isPlaceholder = modelData?.num === -1;
|
isPlaceholder = modelData?.num === -1;
|
||||||
} else {
|
} else {
|
||||||
isPlaceholder = modelData === -1;
|
isPlaceholder = modelData === -1;
|
||||||
@@ -665,7 +679,7 @@ Item {
|
|||||||
return getWorkspaceIndexFallback(modelData, index);
|
return getWorkspaceIndexFallback(modelData, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll
|
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
|
||||||
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
|
||||||
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||||
|
|
||||||
@@ -865,7 +879,7 @@ Item {
|
|||||||
return !!(modelData && modelData.id === root.currentWorkspace);
|
return !!(modelData && modelData.id === root.currentWorkspace);
|
||||||
if (CompositorService.isDwl)
|
if (CompositorService.isDwl)
|
||||||
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
|
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
|
||||||
if (CompositorService.isSway || CompositorService.isScroll)
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
return !!(modelData && modelData.num === root.currentWorkspace);
|
return !!(modelData && modelData.num === root.currentWorkspace);
|
||||||
return modelData === root.currentWorkspace;
|
return modelData === root.currentWorkspace;
|
||||||
}
|
}
|
||||||
@@ -889,7 +903,7 @@ Item {
|
|||||||
return !!(modelData && modelData.id === -1);
|
return !!(modelData && modelData.id === -1);
|
||||||
if (CompositorService.isDwl)
|
if (CompositorService.isDwl)
|
||||||
return !!(modelData && modelData.tag === -1);
|
return !!(modelData && modelData.tag === -1);
|
||||||
if (CompositorService.isSway || CompositorService.isScroll)
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
return !!(modelData && modelData.num === -1);
|
return !!(modelData && modelData.num === -1);
|
||||||
return modelData === -1;
|
return modelData === -1;
|
||||||
}
|
}
|
||||||
@@ -906,12 +920,17 @@ Item {
|
|||||||
return loadedIsUrgent;
|
return loadedIsUrgent;
|
||||||
if (CompositorService.isDwl)
|
if (CompositorService.isDwl)
|
||||||
return modelData?.state === 2;
|
return modelData?.state === 2;
|
||||||
if (CompositorService.isSway || CompositorService.isScroll)
|
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
return loadedIsUrgent;
|
return loadedIsUrgent;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
property var loadedIconData: null
|
readonly property var loadedIconData: {
|
||||||
property bool loadedHasIcon: false
|
if (isPlaceholder) return null;
|
||||||
|
const name = modelData?.name;
|
||||||
|
if (!name) return null;
|
||||||
|
return SettingsData.getWorkspaceNameIcon(name);
|
||||||
|
}
|
||||||
|
readonly property bool loadedHasIcon: loadedIconData !== null
|
||||||
property var loadedIcons: []
|
property var loadedIcons: []
|
||||||
|
|
||||||
readonly property int stableIconCount: {
|
readonly property int stableIconCount: {
|
||||||
@@ -927,7 +946,7 @@ Item {
|
|||||||
targetWorkspaceId = modelData?.id;
|
targetWorkspaceId = modelData?.id;
|
||||||
} else if (CompositorService.isDwl) {
|
} else if (CompositorService.isDwl) {
|
||||||
targetWorkspaceId = modelData?.tag;
|
targetWorkspaceId = modelData?.tag;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
targetWorkspaceId = modelData?.num;
|
targetWorkspaceId = modelData?.num;
|
||||||
}
|
}
|
||||||
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
|
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
|
||||||
@@ -946,7 +965,7 @@ Item {
|
|||||||
let winWs = null;
|
let winWs = null;
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
winWs = w.workspace_id;
|
winWs = w.workspace_id;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
winWs = w.workspace?.num;
|
winWs = w.workspace?.num;
|
||||||
} else if (CompositorService.isHyprland) {
|
} else if (CompositorService.isHyprland) {
|
||||||
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
|
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
|
||||||
@@ -971,9 +990,9 @@ Item {
|
|||||||
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
|
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
|
||||||
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
|
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
|
||||||
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
|
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
|
||||||
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri
|
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0
|
readonly property real contentImplicitWidth: hasWorkspaceName ? (appIconsLoader.item?.contentWidth ?? 0) : 0
|
||||||
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
|
readonly property real contentImplicitHeight: workspaceNamesEnabled ? (appIconsLoader.item?.contentHeight ?? 0) : 0
|
||||||
|
|
||||||
readonly property real iconsExtraWidth: {
|
readonly property real iconsExtraWidth: {
|
||||||
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
|
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
|
||||||
@@ -1123,9 +1142,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (!dragHandler.dragging) {
|
if (!dragHandler.dragging) {
|
||||||
const distance = root.isVertical
|
const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
||||||
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
|
|
||||||
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
|
||||||
if (distance > 5) {
|
if (distance > 5) {
|
||||||
dragHandler.dragging = true;
|
dragHandler.dragging = true;
|
||||||
root.dragSourceIndex = index;
|
root.dragSourceIndex = index;
|
||||||
@@ -1136,9 +1153,7 @@ Item {
|
|||||||
if (!dragHandler.dragging)
|
if (!dragHandler.dragging)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const rawAxisOffset = root.isVertical
|
const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
|
||||||
? (mouse.y - dragHandler.dragStartPos.y)
|
|
||||||
: (mouse.x - dragHandler.dragStartPos.x);
|
|
||||||
|
|
||||||
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
|
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
|
||||||
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
|
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
|
||||||
@@ -1189,7 +1204,7 @@ Item {
|
|||||||
Hyprland.dispatch(`workspace ${modelData.id}`);
|
Hyprland.dispatch(`workspace ${modelData.id}`);
|
||||||
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
|
||||||
DwlService.switchToTag(root.screenName, modelData.tag);
|
DwlService.switchToTag(root.screenName, modelData.tag);
|
||||||
} else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) {
|
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
|
||||||
try {
|
try {
|
||||||
I3.dispatch(`workspace number ${modelData.num}`);
|
I3.dispatch(`workspace number ${modelData.num}`);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -1212,8 +1227,6 @@ Item {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (isPlaceholder) {
|
if (isPlaceholder) {
|
||||||
delegateRoot.loadedWorkspaceData = null;
|
delegateRoot.loadedWorkspaceData = null;
|
||||||
delegateRoot.loadedIconData = null;
|
|
||||||
delegateRoot.loadedHasIcon = false;
|
|
||||||
delegateRoot.loadedIcons = [];
|
delegateRoot.loadedIcons = [];
|
||||||
delegateRoot.loadedIsUrgent = false;
|
delegateRoot.loadedIsUrgent = false;
|
||||||
return;
|
return;
|
||||||
@@ -1228,7 +1241,7 @@ Item {
|
|||||||
wsData = modelData;
|
wsData = modelData;
|
||||||
} else if (CompositorService.isDwl) {
|
} else if (CompositorService.isDwl) {
|
||||||
wsData = modelData;
|
wsData = modelData;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
wsData = modelData;
|
wsData = modelData;
|
||||||
}
|
}
|
||||||
delegateRoot.loadedWorkspaceData = wsData;
|
delegateRoot.loadedWorkspaceData = wsData;
|
||||||
@@ -1239,15 +1252,8 @@ Item {
|
|||||||
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var icData = null;
|
|
||||||
if (wsData?.name) {
|
|
||||||
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
|
|
||||||
}
|
|
||||||
delegateRoot.loadedIconData = icData;
|
|
||||||
delegateRoot.loadedHasIcon = icData !== null;
|
|
||||||
|
|
||||||
if (SettingsData.showWorkspaceApps) {
|
if (SettingsData.showWorkspaceApps) {
|
||||||
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) {
|
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
|
||||||
} else if (CompositorService.isNiri) {
|
} else if (CompositorService.isNiri) {
|
||||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
|
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
|
||||||
@@ -1410,7 +1416,7 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: loadedHasIcon && loadedIconData?.type === "icon"
|
visible: loadedHasIcon && loadedIconData?.type === "icon"
|
||||||
width: wsIcon.width + (isActive && loadedIcons.length > 0 ? 4 : 0)
|
width: wsIcon.width
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -1425,7 +1431,7 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: loadedHasIcon && loadedIconData?.type === "text"
|
visible: loadedHasIcon && loadedIconData?.type === "text"
|
||||||
width: wsText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
|
width: wsText.implicitWidth
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
@@ -1439,14 +1445,14 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
|
visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
|
||||||
width: wsIndexText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
|
width: wsIndexText.implicitWidth
|
||||||
height: root.appIconSize
|
height: root.appIconSize
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
id: wsIndexText
|
id: wsIndexText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.getWorkspaceIndex(modelData, index)
|
text: loadedHasIcon ? (modelData?.name ?? "") : root.getWorkspaceIndex(modelData, index)
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||||
@@ -1564,9 +1570,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
|
visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: root.getWorkspaceIndex(modelData, index)
|
text: loadedHasIcon ? (root.isVertical ? (modelData?.name ?? "").charAt(0) : (modelData?.name ?? "")) : root.getWorkspaceIndex(modelData, index)
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
||||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
||||||
@@ -1660,55 +1666,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loader for Custom Name Icon
|
|
||||||
Loader {
|
|
||||||
id: customIconLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
DankIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
|
||||||
weight: isActive && !isPlaceholder ? 500 : 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Custom Name Text
|
|
||||||
Loader {
|
|
||||||
id: customTextLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
|
|
||||||
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loader for Workspace Index
|
|
||||||
Loader {
|
|
||||||
id: indexLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon && !SettingsData.showWorkspaceApps
|
|
||||||
sourceComponent: Item {
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
return root.getWorkspaceIndex(modelData, index);
|
|
||||||
}
|
|
||||||
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
|
|
||||||
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
|
|
||||||
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: updateAllData()
|
Component.onCompleted: updateAllData()
|
||||||
@@ -1760,7 +1717,7 @@ Item {
|
|||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: I3.workspaces
|
target: I3.workspaces
|
||||||
enabled: (CompositorService.isSway || CompositorService.isScroll)
|
enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||||
function onValuesChanged() {
|
function onValuesChanged() {
|
||||||
delegateRoot.updateAllData();
|
delegateRoot.updateAllData();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ DankPopout {
|
|||||||
|
|
||||||
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
implicitHeight: contentColumn.height + Theme.spacingM * 2
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
radius: Theme.cornerRadius
|
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ Card {
|
|||||||
return "on Sway";
|
return "on Sway";
|
||||||
if (CompositorService.isScroll)
|
if (CompositorService.isScroll)
|
||||||
return "on Scroll";
|
return "on Scroll";
|
||||||
|
if (CompositorService.isMiracle)
|
||||||
|
return "on Miracle WM";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ Item {
|
|||||||
showDirsFirst: false
|
showDirsFirst: false
|
||||||
showDotAndDotDot: false
|
showDotAndDotDot: false
|
||||||
showHidden: false
|
showHidden: false
|
||||||
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
caseSensitive: false
|
||||||
|
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||||
showFiles: true
|
showFiles: true
|
||||||
showDirs: false
|
showDirs: false
|
||||||
sortField: FolderListModel.Name
|
sortField: FolderListModel.Name
|
||||||
@@ -320,7 +321,7 @@ Item {
|
|||||||
browserIcon: "folder_open"
|
browserIcon: "folder_open"
|
||||||
browserType: "wallpaper"
|
browserType: "wallpaper"
|
||||||
showHiddenFiles: false
|
showHiddenFiles: false
|
||||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||||
parentPopout: root.parentPopout
|
parentPopout: root.parentPopout
|
||||||
|
|
||||||
onFileSelected: path => {
|
onFileSelected: path => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user