mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-23 03:34:01 -04:00
Compare commits
30
Commits
stable
..
stable-2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec4e04f06f | ||
|
|
5aa2e3f0d4 | ||
|
|
11f99cc13f | ||
|
|
0ae0b96885 | ||
|
|
f0826a953f | ||
|
|
b5da117206 | ||
|
|
430db4e493 | ||
|
|
2a636c7092 | ||
|
|
8848ad82f1 | ||
|
|
404d5362e5 | ||
|
|
4ab57f14b8 | ||
|
|
ce81dfc95a | ||
|
|
a5511fe8d7 | ||
|
|
7d930f8f6c | ||
|
|
11ef76b9d0 | ||
|
|
e48de4cc2a | ||
|
|
1683fceac5 | ||
|
|
2a83c5c69b | ||
|
|
1acfae1755 | ||
|
|
1e1dbebb70 | ||
|
|
53412d1f22 | ||
|
|
9ece8f26fa | ||
|
|
fd1295f959 | ||
|
|
1e3f16ac9d | ||
|
|
4fa6f58720 | ||
|
|
ff45b3b4b2 | ||
|
|
12e54e0225 | ||
|
|
f63d8bf4fb | ||
|
|
1e8423992d | ||
|
|
c4ecb988b4 |
@@ -100,7 +100,7 @@ Notification center with grouping, rich text support, and keyboard navigation.
|
|||||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Session Management**
|
**Session Management**
|
||||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/duxlabs-dev/dux-greeter).
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dux-greeter](https://github.com/duxlabs-dev/dux-greeter).
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
Extend functionality with the [plugin registry](https://plugins.duxlabs.dev). DMS keeps
|
Extend functionality with the [plugin registry](https://plugins.duxlabs.dev). DMS keeps
|
||||||
@@ -138,16 +138,16 @@ dms plugins restore ~/plugins.lock.json
|
|||||||
- **Plugins:** [Development guide](https://duxlabs.dev/docs/dms/plugins-overview)
|
- **Plugins:** [Development guide](https://duxlabs.dev/docs/dms/plugins-overview)
|
||||||
- **Support:** [Ko-fi](https://ko-fi.com/duxlabs)
|
- **Support:** [Ko-fi](https://ko-fi.com/duxlabs)
|
||||||
|
|
||||||
## Dank Projects
|
## Dux Projects
|
||||||
|
|
||||||
DMS is one piece of the suite. The rest lives in its own repos:
|
DMS is one piece of the suite. The rest lives in its own repos:
|
||||||
|
|
||||||
- [dank-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell aesthetic. The Greeter tab in DMS settings is the front-end for it.
|
- [dux-greeter](https://github.com/duxlabs-dev/dux-greeter) - greetd login screen with the Dux Shell aesthetic. The Greeter tab in DMS settings is the front-end for it.
|
||||||
- [dankcalendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
|
- [dux-calendar](https://github.com/duxlabs-dev/dux-calendar) - Local, Google, Microsoft, and CalDAV calendars for the Dux desktop.
|
||||||
- [dgop](https://github.com/duxlabs-dev/dux-gop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
|
- [dgop](https://github.com/duxlabs-dev/dux-gop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
|
||||||
- [dsearch](https://github.com/duxlabs-dev/dux-search) - Fast file search that powers file results in the launcher.
|
- [dsearch](https://github.com/duxlabs-dev/dux-search) - Fast file search that powers file results in the launcher.
|
||||||
- [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
|
- [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) - Shared QML widgets and components used by DMS, dux-greeter, and dux-calendar.
|
||||||
- [dankgo](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
|
- [dux-go](https://github.com/duxlabs-dev/dux-go) - Common Go modules behind the single binary apps.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ Contributions welcome. Bug fixes, widgets, features, documentation, and plugins
|
|||||||
3. Test thoroughly
|
3. Test thoroughly
|
||||||
4. Open a pull request
|
4. Open a pull request
|
||||||
|
|
||||||
For documentation contributions, see [DankLinux-Docs](https://github.com/duxlabs-dev/dux-docs).
|
For documentation contributions, see [dux-docs](https://github.com/duxlabs-dev/dux-docs).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|||||||
@@ -55,32 +55,8 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
Run: runResolveInclude,
|
Run: runResolveInclude,
|
||||||
}
|
}
|
||||||
|
|
||||||
var mangoMigrateMain bool
|
|
||||||
|
|
||||||
var mangoMigrateCmd = &cobra.Command{
|
|
||||||
Use: "mango-migrate",
|
|
||||||
Short: "Respell Mango config keys for the installed Mango",
|
|
||||||
Long: "Detects whether the installed Mango uses legacy or snake_case config keys, rewrites the DMS fragments in mango/dms to match, and prints the dialect on the last line. --main also rewrites config.conf after a backup.",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
dialect := mangoconf.Detect()
|
|
||||||
changed, err := dialect.Migrate(mangoconf.Dir(), mangoMigrateMain)
|
|
||||||
for _, path := range changed {
|
|
||||||
fmt.Fprintf(os.Stderr, "migrated %s\n", path)
|
|
||||||
}
|
|
||||||
// Printed before any error: the shell needs the dialect even when a fragment failed.
|
|
||||||
fmt.Println(dialect)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
configCmd.AddCommand(resolveIncludeCmd)
|
configCmd.AddCommand(resolveIncludeCmd)
|
||||||
mangoMigrateCmd.Flags().BoolVar(&mangoMigrateMain, "main", false, "also rewrite config.conf (a backup is kept)")
|
|
||||||
configCmd.AddCommand(mangoMigrateCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IncludeResult struct {
|
type IncludeResult struct {
|
||||||
|
|||||||
@@ -858,6 +858,14 @@ func checkWindowManagers() []checkResult {
|
|||||||
if c.name == "Hyprland" {
|
if c.name == "Hyprland" {
|
||||||
results = append(results, checkHyprlandConfigFormat(filepath.Join(utils.XDGConfigHome(), "hypr"))...)
|
results = append(results, checkHyprlandConfigFormat(filepath.Join(utils.XDGConfigHome(), "hypr"))...)
|
||||||
}
|
}
|
||||||
|
if c.name == "mangowc" && mangoLacksSnakeCase(ver) {
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catCompositor, "Mango config format", statusError,
|
||||||
|
fmt.Sprintf("Mango %s is older than 0.18", ver),
|
||||||
|
"DMS writes Mango config with the snake_case keys of Mango 0.18 and later, which this release rejects; upgrade Mango",
|
||||||
|
doctorDocsURL + "#compositor-checks",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foundAny {
|
if !foundAny {
|
||||||
@@ -878,7 +886,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
|
if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" {
|
||||||
results = append(results, checkMangoConfig(mangoconf.Dir(), mangoconf.Detect())...)
|
results = append(results, checkMangoConfig(mangoconf.Dir())...)
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, checkCompositorBlurSupport())
|
results = append(results, checkCompositorBlurSupport())
|
||||||
@@ -886,33 +894,70 @@ func checkWindowManagers() []checkResult {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
var mangoLegacyOverviewBinds = regexp.MustCompile(`(?im)^\s*mousebind\s*=\s*none\s*,\s*(btn_left\s*,\s*toggleoverview\s*,\s*1|btn_right\s*,\s*killclient\s*,\s*0)\s*$`)
|
var (
|
||||||
|
mangoLegacyOverviewBinds = regexp.MustCompile(`(?im)^\s*mousebind\s*=\s*none\s*,\s*(btn_left\s*,\s*toggleoverview\s*,\s*1|btn_right\s*,\s*killclient\s*,\s*0)\s*$`)
|
||||||
|
mangoUnknownKeyword = regexp.MustCompile(`Unknown keyword:\s*(\S+)`)
|
||||||
|
ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||||
|
)
|
||||||
|
|
||||||
func checkMangoConfig(mangoDir string, dialect mangoconf.Dialect) []checkResult {
|
// Mango's parser follows source= and always exits 0; only its output tells.
|
||||||
|
var mangoParseConfig = func(path string) (string, error) {
|
||||||
|
if _, err := exec.LookPath("mango"); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out, _ := exec.Command("mango", "-p", "-c", path).CombinedOutput()
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each "Unknown keyword" line is followed by a location line naming the file.
|
||||||
|
func mangoUnknownKeywords(output string) []string {
|
||||||
|
var found []string
|
||||||
|
seen := map[string]bool{}
|
||||||
|
key := ""
|
||||||
|
for line := range strings.SplitSeq(ansiEscape.ReplaceAllString(output, ""), "\n") {
|
||||||
|
if m := mangoUnknownKeyword.FindStringSubmatch(line); m != nil {
|
||||||
|
key = m[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slash := strings.LastIndex(line, "/")
|
||||||
|
if key == "" || slash < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, _, _ := strings.Cut(line[slash+1:], ":")
|
||||||
|
entry := key + " (" + file + ")"
|
||||||
|
if !seen[entry] {
|
||||||
|
seen[entry] = true
|
||||||
|
found = append(found, entry)
|
||||||
|
}
|
||||||
|
key = ""
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMangoConfig(mangoDir string) []checkResult {
|
||||||
url := doctorDocsURL + "#compositor-checks"
|
url := doctorDocsURL + "#compositor-checks"
|
||||||
paths, _ := filepath.Glob(filepath.Join(mangoDir, "dms", "*.conf"))
|
mainPath := filepath.Join(mangoDir, "config.conf")
|
||||||
paths = append([]string{filepath.Join(mangoDir, "config.conf")}, paths...)
|
var results []checkResult
|
||||||
|
if output, err := mangoParseConfig(mainPath); err == nil {
|
||||||
|
if unknown := mangoUnknownKeywords(output); len(unknown) > 0 {
|
||||||
|
results = append(results, checkResult{catCompositor, "Mango config keys", statusError,
|
||||||
|
"Unknown keys: " + strings.Join(unknown, ", "),
|
||||||
|
"Mango 0.18 renamed its config keys to snake_case. Run migrate-config-keys.sh from Mango's 0.18.0 release over ~/.config/mango, or rename the keys by hand.", url})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var stale, overviewBinds []string
|
paths, _ := filepath.Glob(filepath.Join(mangoDir, "dms", "*.conf"))
|
||||||
|
paths = append([]string{mainPath}, paths...)
|
||||||
|
var overviewBinds []string
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if dialect.Translate(string(data)) != string(data) {
|
|
||||||
stale = append(stale, filepath.Base(path))
|
|
||||||
}
|
|
||||||
if mangoLegacyOverviewBinds.Match(data) {
|
if mangoLegacyOverviewBinds.Match(data) {
|
||||||
overviewBinds = append(overviewBinds, filepath.Base(path))
|
overviewBinds = append(overviewBinds, filepath.Base(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
|
||||||
if len(stale) > 0 {
|
|
||||||
results = append(results, checkResult{catCompositor, "Mango config keys", statusError,
|
|
||||||
fmt.Sprintf("Not %s keys: %s", dialect, strings.Join(stale, ", ")),
|
|
||||||
"This Mango rejects those lines as unknown keywords. Run `dms config mango-migrate --main` (config.conf is backed up).", url})
|
|
||||||
}
|
|
||||||
if len(overviewBinds) > 0 {
|
if len(overviewBinds) > 0 {
|
||||||
results = append(results, checkResult{catCompositor, "Mango mouse binds", statusWarn,
|
results = append(results, checkResult{catCompositor, "Mango mouse binds", statusWarn,
|
||||||
"Obsolete overview binds in " + strings.Join(overviewBinds, ", "),
|
"Obsolete overview binds in " + strings.Join(overviewBinds, ", "),
|
||||||
@@ -967,6 +1012,10 @@ func hyprlandLacksLua(ver string) bool {
|
|||||||
return ver != "" && version.CompareVersions(ver, "0.55.0") < 0
|
return ver != "" && version.CompareVersions(ver, "0.55.0") < 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mangoLacksSnakeCase(ver string) bool {
|
||||||
|
return ver != "" && version.CompareVersions(ver, "0.18.0") < 0
|
||||||
|
}
|
||||||
|
|
||||||
func firstSubmatch(re *regexp.Regexp) func(string) string {
|
func firstSubmatch(re *regexp.Regexp) func(string) string {
|
||||||
return func(s string) string {
|
return func(s string) string {
|
||||||
if m := re.FindStringSubmatch(s); len(m) > 1 {
|
if m := re.FindStringSubmatch(s); len(m) > 1 {
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQuickshellVersionFailureDetails(t *testing.T) {
|
func TestQuickshellVersionFailureDetails(t *testing.T) {
|
||||||
@@ -385,19 +383,41 @@ func TestCheckXDGMenuFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckMangoConfigFlagsWrongDialectAndOverviewBinds(t *testing.T) {
|
func TestCheckMangoConfigReportsUnknownKeysAndOverviewBinds(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
if err := os.MkdirAll(filepath.Join(dir, "dms"), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(dir, "dms"), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_ = os.WriteFile(filepath.Join(dir, "config.conf"), []byte("exec-once=dms run\nmousebind=NONE,btn_left,toggleoverview,1\n"), 0o644)
|
_ = os.WriteFile(filepath.Join(dir, "config.conf"), []byte("exec-once=dms run\nmousebind=NONE,btn_left,toggleoverview,1\n"), 0o644)
|
||||||
_ = os.WriteFile(filepath.Join(dir, "dms", "layout.conf"), []byte("border_px=2\n"), 0o644)
|
// Shape of `mango -p` 0.18.2 output, colours included; the same key twice collapses.
|
||||||
|
parsed := "\x1b[1m\x1b[31m[ERROR]:\x1b[0m Unknown keyword: \x1b[1m\x1b[31mexec-once\x1b[0m\n" +
|
||||||
|
"\x1b[1;31m╰─\x1b[1;33m[Index]\x1b[0m \x1b[1;36m" + dir + "/config.conf\x1b[0m:\x1b[1;35m1\x1b[0m\n ╰─exec-once=dms run\n\n" +
|
||||||
|
"[ERROR]: Unknown keyword: overviewgappo\n╰─[Index] " + dir + "/dms/layout.conf:10\n ╰─overviewgappo=30\n" +
|
||||||
|
"[ERROR]: Unknown keyword: overviewgappo\n╰─[Index] " + dir + "/dms/layout.conf:12\n"
|
||||||
|
prev := mangoParseConfig
|
||||||
|
mangoParseConfig = func(string) (string, error) { return parsed, nil }
|
||||||
|
t.Cleanup(func() { mangoParseConfig = prev })
|
||||||
|
|
||||||
if got := checkMangoConfig(dir, mangoconf.Legacy); len(got) != 2 || got[0].message != "Not legacy keys: layout.conf" {
|
got := checkMangoConfig(dir)
|
||||||
t.Fatalf("legacy: %+v", got)
|
if len(got) != 2 || got[0].message != "Unknown keys: exec-once (config.conf), overviewgappo (layout.conf)" || got[1].name != "Mango mouse binds" {
|
||||||
|
t.Fatalf("got %+v", got)
|
||||||
}
|
}
|
||||||
if got := checkMangoConfig(dir, mangoconf.Snake); len(got) != 2 || got[0].message != "Not snake keys: config.conf" {
|
mangoParseConfig = func(string) (string, error) { return "", os.ErrNotExist }
|
||||||
t.Fatalf("snake: %+v", got)
|
if got := checkMangoConfig(dir); len(got) != 1 {
|
||||||
|
t.Fatalf("without mango on PATH only the bind check should run, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMangoLacksSnakeCase(t *testing.T) {
|
||||||
|
for ver, want := range map[string]bool{
|
||||||
|
"0.17.5": true,
|
||||||
|
"0.18.0": false,
|
||||||
|
"0.18.2": false,
|
||||||
|
"": false,
|
||||||
|
} {
|
||||||
|
if got := mangoLacksSnakeCase(ver); got != want {
|
||||||
|
t.Errorf("mangoLacksSnakeCase(%q) = %v, want %v", ver, got, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
mangoFile: "layout.conf",
|
mangoFile: "layout.conf",
|
||||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||||
mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoLayoutConfig) },
|
mangoContent: func(_ string) string { return config.MangoLayoutConfig },
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
niriFile: "colors.kdl",
|
niriFile: "colors.kdl",
|
||||||
@@ -138,7 +138,7 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
mangoFile: "colors.conf",
|
mangoFile: "colors.conf",
|
||||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||||
mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoColorsConfig) },
|
mangoContent: func(_ string) string { return config.MangoColorsConfig },
|
||||||
},
|
},
|
||||||
"alttab": {
|
"alttab": {
|
||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/duxlabs-dev/dux-go/shellapp"
|
"github.com/duxlabs-dev/dux-go/shellapp"
|
||||||
|
"github.com/duxlabs-dev/dux-shell/core/internal/clipolicy"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/config"
|
"github.com/duxlabs-dev/dux-shell/core/internal/config"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/gpu"
|
"github.com/duxlabs-dev/dux-shell/core/internal/gpu"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/server"
|
"github.com/duxlabs-dev/dux-shell/core/internal/server"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/shellembed"
|
"github.com/duxlabs-dev/dux-shell/core/internal/shellembed"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
var shellApp = shellapp.New(shellapp.Config{
|
var shellApp = shellapp.New(shellapp.Config{
|
||||||
@@ -83,18 +87,47 @@ func preLaunch() {
|
|||||||
ensureFontCache()
|
ensureFontCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The elapsed-timer driver keeps the scrolling wallpaper spring in step above 60 Hz,
|
||||||
|
// but it makes every other animation jump after a dropped frame (#3837).
|
||||||
|
func scrollingWallpaperConfigured() bool {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filepath.Join(configDir, "dms", "settings.json"))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return wantsSimpleAnimationDriver(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wantsSimpleAnimationDriver(settings []byte) bool {
|
||||||
|
var parsed struct {
|
||||||
|
WallpaperFillMode string `json:"wallpaperFillMode"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(settings, &parsed) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return parsed.WallpaperFillMode == "Scrolling"
|
||||||
|
}
|
||||||
|
|
||||||
func dmsExtraEnv(string) []string {
|
func dmsExtraEnv(string) []string {
|
||||||
var env []string
|
var env []string
|
||||||
if selfPath, err := os.Executable(); err == nil {
|
if selfPath, err := os.Executable(); err == nil {
|
||||||
env = append(env, "DMS_EXECUTABLE="+selfPath)
|
env = append(env, "DMS_EXECUTABLE="+selfPath)
|
||||||
}
|
}
|
||||||
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
|
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" && scrollingWallpaperConfigured() {
|
||||||
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
||||||
}
|
}
|
||||||
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
|
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
|
||||||
env = append(env, "MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000")
|
env = append(env, "MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000")
|
||||||
}
|
}
|
||||||
env = append(env, gpu.EGLVendorEnv()...)
|
env = append(env, gpu.EGLVendorEnv()...)
|
||||||
|
policyEnv, err := clipolicy.ShellEnv(afero.NewOsFs())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("CLI policy unreadable, enforcing every shell switch: %v", err)
|
||||||
|
}
|
||||||
|
env = append(env, policyEnv...)
|
||||||
// systemd user services miss the login shell PATH, so tools in ~/.local/bin
|
// systemd user services miss the login shell PATH, so tools in ~/.local/bin
|
||||||
// (herdr, pip/cargo installs) would be invisible to the shell and its children.
|
// (herdr, pip/cargo installs) would be invisible to the shell and its children.
|
||||||
for _, entry := range utils.EnvWithUserBinPath(nil) {
|
for _, entry := range utils.EnvWithUserBinPath(nil) {
|
||||||
|
|||||||
@@ -20,9 +20,16 @@ var runSystemctl = func(args ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assigned in init: shellApp's config refers back to trySystemdRestart.
|
||||||
|
var shellRunning func() bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
shellRunning = func() bool { return len(shellApp.PIDs()) > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
func trySystemdRestart() (bool, error) {
|
func trySystemdRestart() (bool, error) {
|
||||||
if err := runSystemctl("--user", "is-active", "--quiet", dmsSystemdUnit); err != nil {
|
if err := runSystemctl("--user", "is-active", "--quiet", dmsSystemdUnit); err != nil {
|
||||||
return false, nil
|
return trySystemdStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSystemctl("--user", "restart", dmsSystemdUnit); err != nil {
|
if err := runSystemctl("--user", "restart", dmsSystemdUnit); err != nil {
|
||||||
@@ -30,3 +37,23 @@ func trySystemdRestart() (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With nothing running, restart would spawn an unmanaged daemon that blocks
|
||||||
|
// the unit once the session target starts it. A running hand-started
|
||||||
|
// instance is left to the plain restart path.
|
||||||
|
func trySystemdStart() (bool, error) {
|
||||||
|
if shellRunning() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := runSystemctl("--user", "is-enabled", "--quiet", dmsSystemdUnit); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := runSystemctl("--user", "is-active", "--quiet", "graphical-session.target"); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSystemctl("--user", "start", dmsSystemdUnit); err != nil {
|
||||||
|
return true, fmt.Errorf("starting %s: %w", dmsSystemdUnit, err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrySystemdRestart(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
running bool
|
||||||
|
ok []string
|
||||||
|
wantHandled bool
|
||||||
|
wantCall string
|
||||||
|
}{
|
||||||
|
{"active unit restarts", false, []string{"is-active dms.service", "restart"}, true, "restart dms.service"},
|
||||||
|
{"enabled inactive unit starts", false, []string{"is-enabled", "is-active graphical-session.target", "start"}, true, "start dms.service"},
|
||||||
|
{"hand-started instance keeps plain restart", true, []string{"is-enabled", "is-active graphical-session.target", "start"}, false, ""},
|
||||||
|
{"session target not up", false, []string{"is-enabled"}, false, ""},
|
||||||
|
{"unit disabled", false, []string{"is-active graphical-session.target"}, false, ""},
|
||||||
|
{"no systemd", false, nil, false, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
origSystemctl, origRunning := runSystemctl, shellRunning
|
||||||
|
t.Cleanup(func() { runSystemctl, shellRunning = origSystemctl, origRunning })
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
shellRunning = func() bool { return tt.running }
|
||||||
|
var calls []string
|
||||||
|
runSystemctl = func(args ...string) error {
|
||||||
|
call := strings.Join(slices.DeleteFunc(slices.Clone(args), func(a string) bool {
|
||||||
|
return strings.HasPrefix(a, "--")
|
||||||
|
}), " ")
|
||||||
|
calls = append(calls, call)
|
||||||
|
for _, prefix := range tt.ok {
|
||||||
|
if strings.HasPrefix(call, prefix) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("exit status 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
handled, err := trySystemdRestart()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if handled != tt.wantHandled {
|
||||||
|
t.Fatalf("handled = %v, want %v (calls %q)", handled, tt.wantHandled, calls)
|
||||||
|
}
|
||||||
|
if tt.wantCall != "" && !slices.Contains(calls, tt.wantCall) {
|
||||||
|
t.Fatalf("missing %q in calls %q", tt.wantCall, calls)
|
||||||
|
}
|
||||||
|
if !tt.wantHandled && slices.ContainsFunc(calls, func(c string) bool { return strings.HasPrefix(c, "start") }) {
|
||||||
|
t.Fatalf("unexpected start in calls %q", calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@ require (
|
|||||||
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/dlclark/regexp2/v2 v2.9.1
|
github.com/dlclark/regexp2/v2 v2.9.1
|
||||||
github.com/duxlabs-dev/dux-go v1.6.3-0.20261020163124-e2ca52bdf500
|
github.com/duxlabs-dev/dux-go v1.6.3-0.20261023041949-220eb5ff21dc
|
||||||
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea
|
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/go-git/go-git/v6 v6.0.0-beta.1
|
github.com/go-git/go-git/v6 v6.0.0-beta.1
|
||||||
|
|||||||
+2
-2
@@ -97,8 +97,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez
|
|||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||||
github.com/dlclark/regexp2/v2 v2.9.1 h1:q6bZW/NN9VodqMjYXpuic2lZxdHPfFTzGS+MJxIJmKQ=
|
github.com/dlclark/regexp2/v2 v2.9.1 h1:q6bZW/NN9VodqMjYXpuic2lZxdHPfFTzGS+MJxIJmKQ=
|
||||||
github.com/dlclark/regexp2/v2 v2.9.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
github.com/dlclark/regexp2/v2 v2.9.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||||
github.com/duxlabs-dev/dux-go v1.6.3-0.20261020163124-e2ca52bdf500 h1:y02m7QgoX4rwauY9HePIPGCRDJ/fMirBmgj78IkeUsc=
|
github.com/duxlabs-dev/dux-go v1.6.3-0.20261023041949-220eb5ff21dc h1:oZhWdM7mlEevEm1zaeOeDfIMQkvtZSW8UNqrAZldbAY=
|
||||||
github.com/duxlabs-dev/dux-go v1.6.3-0.20261020163124-e2ca52bdf500/go.mod h1:b+hBqIgOVAmlFKJ+33SY2pH4kJjXpifNeioPMxwC4Nk=
|
github.com/duxlabs-dev/dux-go v1.6.3-0.20261023041949-220eb5ff21dc/go.mod h1:b+hBqIgOVAmlFKJ+33SY2pH4kJjXpifNeioPMxwC4Nk=
|
||||||
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea h1:+z9uPtBmpXXBPupB6++/8avPA0weOkzAz7jBoE+yIok=
|
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea h1:+z9uPtBmpXXBPupB6++/8avPA0weOkzAz7jBoE+yIok=
|
||||||
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea/go.mod h1:IYu4GO7zR61w3+tDmjg71dQDInXe5TcP3lHQqJpZYR0=
|
github.com/duxlabs-dev/dux-gop v1.6.3-0.20261020163819-0e4888f1fcea/go.mod h1:IYu4GO7zR61w3+tDmjg71dQDInXe5TcP3lHQqJpZYR0=
|
||||||
github.com/ebitengine/purego v0.11.2 h1:WWiTx8rD8zYdOwSj8O/NiNbsOvSAgJMTlyV1XlbUSeU=
|
github.com/ebitengine/purego v0.11.2 h1:WWiTx8rD8zYdOwSj8O/NiNbsOvSAgJMTlyV1XlbUSeU=
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
@@ -14,11 +15,28 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
PolicyVersion int `json:"policy_version"`
|
PolicyVersion int `json:"policy_version"`
|
||||||
ImmutableSystem *bool `json:"immutable_system"`
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
BlockedCommands *[]string `json:"blocked_commands"`
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
Message *string `json:"message"`
|
Message *string `json:"message"`
|
||||||
DisableRegistries *bool `json:"disable_registries"`
|
DisableRegistries *bool `json:"disable_registries"`
|
||||||
|
DisableLockIPCUnlock *bool `json:"disable_lock_ipc_unlock"`
|
||||||
|
DisableLogindUnlock *bool `json:"disable_logind_unlock"`
|
||||||
|
DisableCustomLockPAM *bool `json:"disable_custom_lock_pam"`
|
||||||
|
DisableUserPlugins *bool `json:"disable_user_plugins"`
|
||||||
|
DisableSettingsIPCSet *bool `json:"disable_settings_ipc_set"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellSwitches reach the shell as DMS_POLICY_<KEY> in its launch environment.
|
||||||
|
var shellSwitches = []struct {
|
||||||
|
key string
|
||||||
|
value func(*File) *bool
|
||||||
|
}{
|
||||||
|
{"disable_lock_ipc_unlock", func(p *File) *bool { return p.DisableLockIPCUnlock }},
|
||||||
|
{"disable_logind_unlock", func(p *File) *bool { return p.DisableLogindUnlock }},
|
||||||
|
{"disable_custom_lock_pam", func(p *File) *bool { return p.DisableCustomLockPAM }},
|
||||||
|
{"disable_user_plugins", func(p *File) *bool { return p.DisableUserPlugins }},
|
||||||
|
{"disable_settings_ipc_set", func(p *File) *bool { return p.DisableSettingsIPCSet }},
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFile returns nil without an error when the file does not exist.
|
// LoadFile returns nil without an error when the file does not exist.
|
||||||
@@ -39,18 +57,65 @@ func LoadFile(fs afero.Fs, path string) (*File, error) {
|
|||||||
return &policy, nil
|
return &policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistriesDisabled reports whether the policy turns off the plugin and theme
|
// load returns the policy files that exist, PackagedPath first.
|
||||||
// registries. An explicit value in AdminPath overrides PackagedPath.
|
func load(fs afero.Fs) ([]*File, error) {
|
||||||
func RegistriesDisabled(fs afero.Fs) (bool, error) {
|
var policies []*File
|
||||||
disabled := false
|
|
||||||
for _, path := range []string{PackagedPath, AdminPath} {
|
for _, path := range []string{PackagedPath, AdminPath} {
|
||||||
policy, err := LoadFile(fs, path)
|
policy, err := LoadFile(fs, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if policy != nil && policy.DisableRegistries != nil {
|
if policy != nil {
|
||||||
disabled = *policy.DisableRegistries
|
policies = append(policies, policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return disabled, nil
|
return policies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag reports a boolean key, false unless set. An explicit value in AdminPath
|
||||||
|
// overrides PackagedPath.
|
||||||
|
func flag(policies []*File, value func(*File) *bool) bool {
|
||||||
|
set := false
|
||||||
|
for _, policy := range policies {
|
||||||
|
if v := value(policy); v != nil {
|
||||||
|
set = *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegistriesDisabled reports whether the policy turns off the plugin and theme
|
||||||
|
// registries. An explicit value in AdminPath overrides PackagedPath.
|
||||||
|
func RegistriesDisabled(fs afero.Fs) (bool, error) {
|
||||||
|
policies, err := load(fs)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return flag(policies, func(p *File) *bool { return p.DisableRegistries }), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogindUnlockDisabled reports whether a logind Unlock must leave a lock in
|
||||||
|
// place. An explicit value in AdminPath overrides PackagedPath.
|
||||||
|
func LogindUnlockDisabled(fs afero.Fs) (bool, error) {
|
||||||
|
policies, err := load(fs)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return flag(policies, func(p *File) *bool { return p.DisableLogindUnlock }), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShellEnv returns every shell switch as DMS_POLICY_<KEY>=1 or 0, so a value
|
||||||
|
// inherited by the shell never counts. A policy file that fails to load turns
|
||||||
|
// every switch on, and its error is returned with them.
|
||||||
|
func ShellEnv(fs afero.Fs) ([]string, error) {
|
||||||
|
policies, err := load(fs)
|
||||||
|
env := make([]string, 0, len(shellSwitches))
|
||||||
|
for _, s := range shellSwitches {
|
||||||
|
value := "0"
|
||||||
|
if err != nil || flag(policies, s.value) {
|
||||||
|
value = "1"
|
||||||
|
}
|
||||||
|
env = append(env, "DMS_POLICY_"+strings.ToUpper(s.key)+"="+value)
|
||||||
|
}
|
||||||
|
return env, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package clipolicy
|
package clipolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
@@ -45,3 +46,103 @@ func TestRegistriesDisabled(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogindUnlockDisabled(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
packaged string
|
||||||
|
admin string
|
||||||
|
want bool
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "no policy files"},
|
||||||
|
{name: "admin policy", admin: `{"disable_logind_unlock": true}`, want: true},
|
||||||
|
{name: "admin policy overrides packaged", packaged: `{"disable_logind_unlock": true}`, admin: `{"disable_logind_unlock": false}`},
|
||||||
|
{name: "another key", admin: `{"disable_lock_ipc_unlock": true}`},
|
||||||
|
{name: "malformed admin policy", admin: "{not json", wantErr: "failed to parse " + AdminPath},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
if tt.packaged != "" {
|
||||||
|
require.NoError(t, afero.WriteFile(fs, PackagedPath, []byte(tt.packaged), 0o644))
|
||||||
|
}
|
||||||
|
if tt.admin != "" {
|
||||||
|
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte(tt.admin), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled, err := LogindUnlockDisabled(fs)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, disabled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shellSwitchEnv = map[string]string{
|
||||||
|
"disable_lock_ipc_unlock": "DMS_POLICY_DISABLE_LOCK_IPC_UNLOCK",
|
||||||
|
"disable_logind_unlock": "DMS_POLICY_DISABLE_LOGIND_UNLOCK",
|
||||||
|
"disable_custom_lock_pam": "DMS_POLICY_DISABLE_CUSTOM_LOCK_PAM",
|
||||||
|
"disable_user_plugins": "DMS_POLICY_DISABLE_USER_PLUGINS",
|
||||||
|
"disable_settings_ipc_set": "DMS_POLICY_DISABLE_SETTINGS_IPC_SET",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellEnv(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
packaged string
|
||||||
|
admin string
|
||||||
|
on bool
|
||||||
|
}{
|
||||||
|
{name: "no policy files"},
|
||||||
|
{name: "packaged policy", packaged: `{"KEY": true}`, on: true},
|
||||||
|
{name: "admin policy", admin: `{"KEY": true}`, on: true},
|
||||||
|
{name: "admin policy overrides packaged", packaged: `{"KEY": true}`, admin: `{"KEY": false}`},
|
||||||
|
{name: "admin policy without the key keeps packaged", packaged: `{"KEY": true}`, admin: `{"immutable_system": true}`, on: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, name := range shellSwitchEnv {
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(key+"/"+tt.name, func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
if tt.packaged != "" {
|
||||||
|
require.NoError(t, afero.WriteFile(fs, PackagedPath, []byte(strings.ReplaceAll(tt.packaged, "KEY", key)), 0o644))
|
||||||
|
}
|
||||||
|
if tt.admin != "" {
|
||||||
|
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte(strings.ReplaceAll(tt.admin, "KEY", key)), 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
var want []string
|
||||||
|
for _, other := range shellSwitchEnv {
|
||||||
|
if other == name && tt.on {
|
||||||
|
want = append(want, other+"=1")
|
||||||
|
} else {
|
||||||
|
want = append(want, other+"=0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := ShellEnv(fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, want, env)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellEnvMalformedPolicyTurnsEverySwitchOn(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, afero.WriteFile(fs, AdminPath, []byte("{not json"), 0o644))
|
||||||
|
|
||||||
|
var want []string
|
||||||
|
for _, name := range shellSwitchEnv {
|
||||||
|
want = append(want, name+"=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := ShellEnv(fs)
|
||||||
|
require.ErrorContains(t, err, "failed to parse "+AdminPath)
|
||||||
|
assert.ElementsMatch(t, want, env)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Auto-generated by DMS. Overwritten by matugen (dms/colors.conf).
|
# Auto-generated by DMS. Overwritten by matugen (dms/colors.conf).
|
||||||
# Remove `source=./dms/colors.conf` from config.conf to override manually.
|
# Remove `source=./dms/colors.conf` from config.conf to override manually.
|
||||||
|
|
||||||
bordercolor = 0x595959ff
|
border_color = 0x595959ff
|
||||||
focuscolor = 0x8ab4f8ff
|
focus_color = 0x8ab4f8ff
|
||||||
urgentcolor = 0xff5555ff
|
urgent_color = 0xff5555ff
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf).
|
# Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf).
|
||||||
|
|
||||||
border_radius=12
|
border_radius=12
|
||||||
gappih=5
|
gap_inner_horizontal=5
|
||||||
gappiv=5
|
gap_inner_vertical=5
|
||||||
gappoh=5
|
gap_outer_horizontal=5
|
||||||
gappov=5
|
gap_outer_vertical=5
|
||||||
borderpx=2
|
border_px=2
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ env=XDG_SESSION_TYPE,wayland
|
|||||||
|
|
||||||
# Starts the shell when DMS is not a systemd service. Never exec=: Mango re-runs
|
# Starts the shell when DMS is not a systemd service. Never exec=: Mango re-runs
|
||||||
# exec= on every config reload, and DMS reloads the config.
|
# exec= on every config reload, and DMS reloads the config.
|
||||||
exec-once=dms run
|
exec_once=dms run
|
||||||
|
|
||||||
source=./dms/colors.conf
|
source=./dms/colors.conf
|
||||||
source=./dms/layout.conf
|
source=./dms/layout.conf
|
||||||
|
|||||||
@@ -41,14 +41,13 @@ func nonEmptyFile(path string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mangoBindLine = regexp.MustCompile(`^\s*(bind[a-z]*|mousebind|axisbind|gesturebind|switchbind|keymode|key_mode)\s*=`)
|
mangoBindLine = regexp.MustCompile(`^\s*(bind[a-z]*|mousebind|axisbind|gesturebind|switchbind|key_mode)\s*=`)
|
||||||
mangoMonitorLine = regexp.MustCompile(`^\s*(monitorrule|monitor_rule)\s*=`)
|
mangoMonitorLine = regexp.MustCompile(`^\s*monitor_rule\s*=`)
|
||||||
mangoWindowLine = regexp.MustCompile(`^\s*(windowrule|window_rule|windowrule-once|window_rule_once)\s*=`)
|
mangoWindowLine = regexp.MustCompile(`^\s*window_rule(_once)?\s*=`)
|
||||||
mangoExecOnceDMS = regexp.MustCompile(`(?m)^# Starts the shell when DMS.*\n#.*\n(exec-once|exec_once)=dms run\n`)
|
mangoExecOnceDMS = regexp.MustCompile(`(?m)^# Starts the shell when DMS.*\n#.*\nexec_once=dms run\n`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mangoDialect = mangoconf.Detect
|
|
||||||
mangoSessionTargetInstalled = mangoconf.SessionTargetInstalled
|
mangoSessionTargetInstalled = mangoconf.SessionTargetInstalled
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,17 +109,16 @@ func (cd *ConfigDeployer) deployMangoConfig(terminalCommand string, useSystemd b
|
|||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
dialect := mangoDialect()
|
|
||||||
newConfig := strings.ReplaceAll(MangoConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
newConfig := strings.ReplaceAll(MangoConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
if cd.systemdAutostart(deps.WindowManagerMango, useSystemd) {
|
if cd.systemdAutostart(deps.WindowManagerMango, useSystemd) {
|
||||||
newConfig = mangoExecOnceDMS.ReplaceAllString(newConfig, "")
|
newConfig = mangoExecOnceDMS.ReplaceAllString(newConfig, "")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(result.Path, []byte(dialect.Translate(newConfig)), 0o644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand, dialect, existing, timestamp); err != nil {
|
if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand, existing, timestamp); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -132,7 +130,7 @@ func (cd *ConfigDeployer) deployMangoConfig(terminalCommand string, useSystemd b
|
|||||||
|
|
||||||
// Like niri, fragments the user or the shell already filled are left alone; the
|
// Like niri, fragments the user or the shell already filled are left alone; the
|
||||||
// replaced config.conf hands its binds, monitor and window rules to empty ones.
|
// replaced config.conf hands its binds, monitor and window rules to empty ones.
|
||||||
func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, dialect mangoconf.Dialect, oldMain, timestamp string) error {
|
func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand, oldMain, timestamp string) error {
|
||||||
stockBinds := strings.ReplaceAll(MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
stockBinds := strings.ReplaceAll(MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||||
if data, err := os.ReadFile(bindsPath); err == nil && len(data) > 0 {
|
if data, err := os.ReadFile(bindsPath); err == nil && len(data) > 0 {
|
||||||
@@ -142,7 +140,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string,
|
|||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing binds to %s", backup))
|
cd.log(fmt.Sprintf("Backed up existing binds to %s", backup))
|
||||||
if cd.replaceBinds {
|
if cd.replaceBinds {
|
||||||
if err := os.WriteFile(bindsPath, []byte(dialect.Translate(stockBinds)), 0o644); err != nil {
|
if err := os.WriteFile(bindsPath, []byte(stockBinds), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write binds.conf: %w", err)
|
return fmt.Errorf("failed to write binds.conf: %w", err)
|
||||||
}
|
}
|
||||||
cd.log("Deployed DMS stock binds.conf")
|
cd.log("Deployed DMS stock binds.conf")
|
||||||
@@ -160,7 +158,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string,
|
|||||||
binds = mangoconf.BindsMovedHeader + "\n" + old
|
binds = mangoconf.BindsMovedHeader + "\n" + old
|
||||||
cd.log("Moved existing binds from config.conf to binds.conf")
|
cd.log("Moved existing binds from config.conf to binds.conf")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(bindsPath, []byte(dialect.Translate(binds)), 0o644); err != nil {
|
if err := os.WriteFile(bindsPath, []byte(binds), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write binds.conf: %w", err)
|
return fmt.Errorf("failed to write binds.conf: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +180,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string,
|
|||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(dialect.Translate(cfg.content)), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stubMango(t *testing.T, dialect mangoconf.Dialect, target bool, addWants error) *int {
|
func stubMango(t *testing.T, target bool, addWants error) *int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
prevDialect, prevTarget := mangoDialect, mangoSessionTargetInstalled
|
prevTarget := mangoSessionTargetInstalled
|
||||||
mangoDialect = func() mangoconf.Dialect { return dialect }
|
|
||||||
mangoSessionTargetInstalled = func() bool { return target }
|
mangoSessionTargetInstalled = func() bool { return target }
|
||||||
t.Cleanup(func() { mangoDialect, mangoSessionTargetInstalled = prevDialect, prevTarget })
|
t.Cleanup(func() { mangoSessionTargetInstalled = prevTarget })
|
||||||
return stubAddWants(t, addWants)
|
return stubAddWants(t, addWants)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,16 +30,16 @@ func readMango(t *testing.T, home, rel string) string {
|
|||||||
|
|
||||||
const oldMangoConfig = `exec-once=waybar
|
const oldMangoConfig = `exec-once=waybar
|
||||||
bind=SUPER,Return,spawn,foot
|
bind=SUPER,Return,spawn,foot
|
||||||
keymode=resize
|
key_mode=resize
|
||||||
bind=NONE,h,resizewin,-10,0
|
bind=NONE,h,resizewin,-10,0
|
||||||
monitorrule=name:^DP-1$,width:2560,height:1440,refresh:144,x:0,y:0,scale:1,rr:0,vrr:1,hdr:1
|
monitor_rule=name:^DP-1$,width:2560,height:1440,refresh:144,x:0,y:0,scale:1,rr:0,vrr:1,hdr:1
|
||||||
windowrule=appid:pavucontrol,isfloating:1
|
window_rule=app_id:pavucontrol,is_floating:1
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) {
|
func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
stubMango(t, mangoconf.Legacy, false, nil)
|
stubMango(t, false, nil)
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(home, ".config", "mango"), 0o755))
|
require.NoError(t, os.MkdirAll(filepath.Join(home, ".config", "mango"), 0o755))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(home, ".config", "mango", "config.conf"), []byte(oldMangoConfig), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(home, ".config", "mango", "config.conf"), []byte(oldMangoConfig), 0o644))
|
||||||
|
|
||||||
@@ -48,25 +47,26 @@ func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotEmpty(t, result.BackupPath)
|
assert.NotEmpty(t, result.BackupPath)
|
||||||
|
|
||||||
assert.Contains(t, readMango(t, home, "config.conf"), "exec-once=dms run")
|
assert.Contains(t, readMango(t, home, "config.conf"), "exec_once=dms run")
|
||||||
assert.Contains(t, readMango(t, home, "config.conf"), "source=./dms/input.conf")
|
assert.Contains(t, readMango(t, home, "config.conf"), "source=./dms/input.conf")
|
||||||
assert.Equal(t, "", readMango(t, home, "dms/input.conf"))
|
assert.Equal(t, "", readMango(t, home, "dms/input.conf"))
|
||||||
binds := readMango(t, home, "dms/binds.conf")
|
binds := readMango(t, home, "dms/binds.conf")
|
||||||
assert.Contains(t, binds, "bind=SUPER,Return,spawn,foot\nkeymode=resize\nbind=NONE,h,resizewin,-10,0")
|
assert.Contains(t, binds, "bind=SUPER,Return,spawn,foot\nkey_mode=resize\nbind=NONE,h,resizewin,-10,0")
|
||||||
assert.NotContains(t, binds, "{{TERMINAL_COMMAND}}")
|
assert.NotContains(t, binds, "{{TERMINAL_COMMAND}}")
|
||||||
|
assert.Contains(t, readMango(t, home, "dms/outputs.conf"), "monitor_rule=name:^DP-1$")
|
||||||
assert.Contains(t, readMango(t, home, "dms/outputs.conf"), "hdr:1")
|
assert.Contains(t, readMango(t, home, "dms/outputs.conf"), "hdr:1")
|
||||||
assert.Contains(t, readMango(t, home, "dms/windowrules.conf"), "windowrule=appid:pavucontrol,isfloating:1")
|
assert.Contains(t, readMango(t, home, "dms/windowrules.conf"), "window_rule=app_id:pavucontrol,is_floating:1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMangoDeployBindsChoiceAlwaysBacksUp(t *testing.T) {
|
func TestMangoDeployBindsChoiceAlwaysBacksUp(t *testing.T) {
|
||||||
for _, replace := range []bool{false, true} {
|
for _, replace := range []bool{false, true} {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
stubMango(t, mangoconf.Legacy, false, nil)
|
stubMango(t, false, nil)
|
||||||
dms := filepath.Join(home, ".config", "mango", "dms")
|
dms := filepath.Join(home, ".config", "mango", "dms")
|
||||||
require.NoError(t, os.MkdirAll(dms, 0o755))
|
require.NoError(t, os.MkdirAll(dms, 0o755))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dms, "binds.conf"), []byte("bind=SUPER,x,killclient\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dms, "binds.conf"), []byte("bind=SUPER,x,killclient\nkey_mode=resize\n"), 0o644))
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dms, "windowrules.conf"), []byte("windowrule=appid:keep,isglobal:1\n"), 0o644))
|
require.NoError(t, os.WriteFile(filepath.Join(dms, "windowrules.conf"), []byte("window_rule=app_id:keep,is_global:1\n"), 0o644))
|
||||||
|
|
||||||
cd := NewConfigDeployer(nil)
|
cd := NewConfigDeployer(nil)
|
||||||
cd.SetReplaceBinds(replace)
|
cd.SetReplaceBinds(replace)
|
||||||
@@ -76,8 +76,8 @@ func TestMangoDeployBindsChoiceAlwaysBacksUp(t *testing.T) {
|
|||||||
backups, _ := filepath.Glob(filepath.Join(dms, "binds.conf.backup.*"))
|
backups, _ := filepath.Glob(filepath.Join(dms, "binds.conf.backup.*"))
|
||||||
assert.Len(t, backups, 1, "replace=%v", replace)
|
assert.Len(t, backups, 1, "replace=%v", replace)
|
||||||
binds := readMango(t, home, "dms/binds.conf")
|
binds := readMango(t, home, "dms/binds.conf")
|
||||||
assert.Equal(t, !replace, binds == mangoconf.BindsKeptHeader+"\nbind=SUPER,x,killclient\n", "replace=%v", replace)
|
assert.Equal(t, !replace, binds == mangoconf.BindsKeptHeader+"\nbind=SUPER,x,killclient\nkey_mode=resize\n", "replace=%v", replace)
|
||||||
assert.Equal(t, "windowrule=appid:keep,isglobal:1\n", readMango(t, home, "dms/windowrules.conf"))
|
assert.Equal(t, "window_rule=app_id:keep,is_global:1\n", readMango(t, home, "dms/windowrules.conf"), "kept fragments are left alone")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func TestMangoDeploySystemdAutostart(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
home := t.TempDir()
|
home := t.TempDir()
|
||||||
t.Setenv("HOME", home)
|
t.Setenv("HOME", home)
|
||||||
calls := stubMango(t, mangoconf.Snake, tc.target, tc.addWants)
|
calls := stubMango(t, tc.target, tc.addWants)
|
||||||
_, err := NewConfigDeployer(nil).DeployCompositor(deps.WindowManagerMango, "kitty", true)
|
_, err := NewConfigDeployer(nil).DeployCompositor(deps.WindowManagerMango, "kitty", true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
main := readMango(t, home, "config.conf")
|
main := readMango(t, home, "config.conf")
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestDisableSessionRemovesEveryWantsLink(t *testing.T) {
|
|||||||
|
|
||||||
func TestEnableSessionMangoWithoutTarget(t *testing.T) {
|
func TestEnableSessionMangoWithoutTarget(t *testing.T) {
|
||||||
t.Setenv("HOME", t.TempDir())
|
t.Setenv("HOME", t.TempDir())
|
||||||
calls := stubMango(t, mangoconf.Snake, false, nil)
|
calls := stubMango(t, false, nil)
|
||||||
unit, err := EnableSession(deps.WindowManagerMango)
|
unit, err := EnableSession(deps.WindowManagerMango)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Empty(t, unit)
|
assert.Empty(t, unit)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Package mangoconf holds what DMS knows about Mango's config files: where they
|
||||||
|
// live, the session target, and the markers DMS leaves in the binds fragment.
|
||||||
|
package mangoconf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mango reads each line into a 512-byte buffer and the value into 255 bytes; longer is cut silently.
|
||||||
|
const maxValueLen = 255
|
||||||
|
|
||||||
|
func CheckLine(line string) error {
|
||||||
|
_, value, ok := strings.Cut(line, "=")
|
||||||
|
if !ok || len(strings.TrimSpace(value)) <= maxValueLen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("line is %d characters after '='; Mango truncates past %d", len(strings.TrimSpace(value)), maxValueLen)
|
||||||
|
}
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
// Package mangoconf maps Mango config keys between the run-together names of
|
|
||||||
// Mango <= 0.17.5 and the snake_case names of upstream 8adc6bc3 (mangowm/mango#1491).
|
|
||||||
// Neither parser accepts the other's names.
|
|
||||||
package mangoconf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dialect int
|
|
||||||
|
|
||||||
const (
|
|
||||||
Legacy Dialect = iota
|
|
||||||
Snake
|
|
||||||
)
|
|
||||||
|
|
||||||
// Upstream migration order: where two legacy names share a snake name, the
|
|
||||||
// window-rule spelling comes first and wins the reverse lookup.
|
|
||||||
var renames = [][2]string{
|
|
||||||
{"windowrule-once", "window_rule_once"},
|
|
||||||
{"windowrule", "window_rule"},
|
|
||||||
{"monitorrule", "monitor_rule"},
|
|
||||||
{"tagrule", "tag_rule"},
|
|
||||||
{"layerrule", "layer_rule"},
|
|
||||||
{"devicerule", "device_rule"},
|
|
||||||
{"source-optional", "source_optional"},
|
|
||||||
{"exec-once", "exec_once"},
|
|
||||||
{"keymode", "key_mode"},
|
|
||||||
{"isnoborder", "no_border"},
|
|
||||||
{"isnoshadow", "no_shadow"},
|
|
||||||
{"isnoradius", "no_radius"},
|
|
||||||
{"isnoanimation", "no_animation"},
|
|
||||||
{"isnosizehint", "no_size_hint"},
|
|
||||||
{"noblur", "no_blur"},
|
|
||||||
{"noshadow", "no_shadow"},
|
|
||||||
{"noanim", "no_animation"},
|
|
||||||
{"nofocus", "no_focus"},
|
|
||||||
{"nofadein", "no_fade_in"},
|
|
||||||
{"nofadeout", "no_fade_out"},
|
|
||||||
{"noswallow", "no_swallow"},
|
|
||||||
{"special_gappih", "special_gap_inner_horizontal"},
|
|
||||||
{"special_gappiv", "special_gap_inner_vertical"},
|
|
||||||
{"special_gappoh", "special_gap_outer_horizontal"},
|
|
||||||
{"special_gappov", "special_gap_outer_vertical"},
|
|
||||||
{"gappih", "gap_inner_horizontal"},
|
|
||||||
{"gappiv", "gap_inner_vertical"},
|
|
||||||
{"gappoh", "gap_outer_horizontal"},
|
|
||||||
{"gappov", "gap_outer_vertical"},
|
|
||||||
{"overviewgappi", "overview_gap_inner"},
|
|
||||||
{"overviewgappo", "overview_gap_outer"},
|
|
||||||
{"default_mfact", "default_master_factor"},
|
|
||||||
{"default_nmaster", "default_master_count"},
|
|
||||||
{"animation_curve_opafadein", "animation_curve_opacity_fade_in"},
|
|
||||||
{"animation_curve_opafadeout", "animation_curve_opacity_fade_out"},
|
|
||||||
{"dwindle_hsplit", "dwindle_horizontal_split"},
|
|
||||||
{"dwindle_vsplit", "dwindle_vertical_split"},
|
|
||||||
{"fadein_begin_opacity", "fade_in_begin_opacity"},
|
|
||||||
{"fadeout_begin_opacity", "fade_out_begin_opacity"},
|
|
||||||
{"focusdir_only_zone_overlap", "focus_direction_only_zone_overlap"},
|
|
||||||
{"force_fakemaximize", "force_fake_maximize"},
|
|
||||||
{"idleinhibit_ignore_visible", "idle_inhibit_ignore_visible"},
|
|
||||||
{"idleinhibit_when_focus", "idle_inhibit_when_focus"},
|
|
||||||
{"idleinhibit_when_fullscreen", "idle_inhibit_when_fullscreen"},
|
|
||||||
{"syncobj_enable", "sync_obj_enable"},
|
|
||||||
{"isfakefullscreen", "is_fake_fullscreen"},
|
|
||||||
{"isfloating", "is_floating"},
|
|
||||||
{"isfullscreen", "is_fullscreen"},
|
|
||||||
{"isunglobal", "is_unmanaged_global"},
|
|
||||||
{"isglobal", "is_global"},
|
|
||||||
{"isnamedscratchpad", "is_named_scratchpad"},
|
|
||||||
{"isopensilent", "is_open_silent"},
|
|
||||||
{"isoverlay", "is_overlay"},
|
|
||||||
{"istagsilent", "is_tag_silent"},
|
|
||||||
{"isterm", "is_term"},
|
|
||||||
{"appid", "app_id"},
|
|
||||||
{"bordercolor", "border_color"},
|
|
||||||
{"borderpx", "border_px"},
|
|
||||||
{"dropcolor", "drop_color"},
|
|
||||||
{"focuscolor", "focus_color"},
|
|
||||||
{"globalcolor", "global_color"},
|
|
||||||
{"globalkeybinding", "global_key_binding"},
|
|
||||||
{"maximizescreencolor", "maximized_screen_color"},
|
|
||||||
{"mfact", "master_factor"},
|
|
||||||
{"nmaster", "master_count"},
|
|
||||||
{"numlockon", "numlock_on"},
|
|
||||||
{"offsetx", "offset_x"},
|
|
||||||
{"offsety", "offset_y"},
|
|
||||||
{"overlaycolor", "overlay_color"},
|
|
||||||
{"rootcolor", "root_color"},
|
|
||||||
{"scratchpadcolor", "scratchpad_color"},
|
|
||||||
{"shadowscolor", "shadows_color"},
|
|
||||||
{"sloppyfocus", "sloppy_focus"},
|
|
||||||
{"smartgaps", "smart_gaps"},
|
|
||||||
{"splitcolor", "split_color"},
|
|
||||||
{"urgentcolor", "urgent_color"},
|
|
||||||
{"warpcursor", "warp_cursor"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var toSnake, toLegacy = func() (map[string]string, map[string]string) {
|
|
||||||
fwd := make(map[string]string, len(renames))
|
|
||||||
rev := make(map[string]string, len(renames))
|
|
||||||
for _, r := range renames {
|
|
||||||
fwd[r[0]] = r[1]
|
|
||||||
if _, ok := rev[r[1]]; !ok {
|
|
||||||
rev[r[1]] = r[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fwd, rev
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Normalize returns the snake_case name for a key in either dialect.
|
|
||||||
func Normalize(key string) string {
|
|
||||||
if s, ok := toSnake[key]; ok {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key spells a snake_case key in this dialect.
|
|
||||||
func (d Dialect) Key(snake string) string {
|
|
||||||
if d == Snake {
|
|
||||||
return snake
|
|
||||||
}
|
|
||||||
if l, ok := toLegacy[snake]; ok {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
return snake
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyLine = regexp.MustCompile(`^(\s*)([A-Za-z][A-Za-z_-]*)(\s*=\s*)(.*)$`)
|
|
||||||
|
|
||||||
// Translate respells every line key, and the field keys of rule lines, in this dialect.
|
|
||||||
func (d Dialect) Translate(text string) string {
|
|
||||||
lines := strings.Split(text, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
m := keyLine.FindStringSubmatch(line)
|
|
||||||
if m == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
key := Normalize(m[2])
|
|
||||||
value := m[4]
|
|
||||||
switch key {
|
|
||||||
case "window_rule", "window_rule_once", "tag_rule":
|
|
||||||
value = d.translateFields(value, nil)
|
|
||||||
case "layer_rule":
|
|
||||||
value = d.translateFields(value, legacyLayerFields)
|
|
||||||
}
|
|
||||||
lines[i] = m[1] + d.Key(key) + m[3] + value
|
|
||||||
}
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer rules spell these without the window rules' "is" prefix.
|
|
||||||
var legacyLayerFields = map[string]string{"no_shadow": "noshadow", "no_animation": "noanim"}
|
|
||||||
|
|
||||||
func (d Dialect) translateFields(value string, legacyOverride map[string]string) string {
|
|
||||||
parts := strings.Split(value, ",")
|
|
||||||
for i, part := range parts {
|
|
||||||
k, v, ok := strings.Cut(part, ":")
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trimmed := strings.TrimSpace(k)
|
|
||||||
key := d.Key(Normalize(trimmed))
|
|
||||||
if l, ok := legacyOverride[Normalize(trimmed)]; ok && d == Legacy {
|
|
||||||
key = l
|
|
||||||
}
|
|
||||||
parts[i] = strings.Replace(k, trimmed, key, 1) + ":" + v
|
|
||||||
}
|
|
||||||
return strings.Join(parts, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy builds carry exec_once as a symbol name, so it cannot tell the dialects apart.
|
|
||||||
var snakeMarker = []byte("gap_inner_horizontal")
|
|
||||||
|
|
||||||
var (
|
|
||||||
detectOnce sync.Once
|
|
||||||
detected Dialect
|
|
||||||
)
|
|
||||||
|
|
||||||
// Detect reads the mango binary: snake_case shipped on master builds still reporting 0.17.5, before the 0.18.0 release.
|
|
||||||
func Detect() Dialect {
|
|
||||||
detectOnce.Do(func() {
|
|
||||||
path, err := exec.LookPath("mango")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
detected = DetectBinary(path)
|
|
||||||
})
|
|
||||||
return detected
|
|
||||||
}
|
|
||||||
|
|
||||||
func DetectBinary(path string) Dialect {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil || !bytes.Contains(data, snakeMarker) {
|
|
||||||
return Legacy
|
|
||||||
}
|
|
||||||
return Snake
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mango reads each line into a 512-byte buffer and the value into 255 bytes; longer is cut silently.
|
|
||||||
const maxValueLen = 255
|
|
||||||
|
|
||||||
func CheckLine(line string) error {
|
|
||||||
_, value, ok := strings.Cut(line, "=")
|
|
||||||
if !ok || len(strings.TrimSpace(value)) <= maxValueLen {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("line is %d characters after '='; Mango truncates past %d", len(strings.TrimSpace(value)), maxValueLen)
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package mangoconf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTranslateRespellsKeysBothWays(t *testing.T) {
|
|
||||||
legacy := "exec-once=dms run\nsource=./dms/colors.conf\ngappih = 5\nbordercolor = 0x595959ff\nwindowrule=appid:foo,isfloating:1,isnoanimation:1\nlayerrule=noanim:1,noshadow:1,layer_name:rofi\n# exec-once=kept\n"
|
|
||||||
snake := "exec_once=dms run\nsource=./dms/colors.conf\ngap_inner_horizontal = 5\nborder_color = 0x595959ff\nwindow_rule=app_id:foo,is_floating:1,no_animation:1\nlayer_rule=no_animation:1,no_shadow:1,layer_name:rofi\n# exec-once=kept\n"
|
|
||||||
assert.Equal(t, snake, Snake.Translate(legacy))
|
|
||||||
assert.Equal(t, legacy, Legacy.Translate(snake))
|
|
||||||
assert.Equal(t, legacy, Legacy.Translate(legacy))
|
|
||||||
assert.Equal(t, "tag_rule=id:1,master_factor:0.6,master_count:2\n", Snake.Translate("tagrule=id:1,mfact:0.6,nmaster:2\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetectBinary(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
old := filepath.Join(dir, "old")
|
|
||||||
cur := filepath.Join(dir, "new")
|
|
||||||
require.NoError(t, os.WriteFile(old, []byte("\x00run_exec_once\x00gappih\x00"), 0o755))
|
|
||||||
require.NoError(t, os.WriteFile(cur, []byte("\x00exec_once\x00gap_inner_horizontal\x00"), 0o755))
|
|
||||||
assert.Equal(t, Legacy, DetectBinary(old))
|
|
||||||
assert.Equal(t, Snake, DetectBinary(cur))
|
|
||||||
assert.Equal(t, Legacy, DetectBinary(filepath.Join(dir, "missing")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMigrateLeavesUserConfigUnlessAsked(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dms"), 0o755))
|
|
||||||
main := filepath.Join(dir, "config.conf")
|
|
||||||
layout := filepath.Join(dir, "dms", "layout.conf")
|
|
||||||
input := filepath.Join(dir, "dms", "input.conf")
|
|
||||||
require.NoError(t, os.WriteFile(main, []byte("exec-once=dms run\n"), 0o644))
|
|
||||||
require.NoError(t, os.WriteFile(layout, []byte("borderpx=2\n"), 0o644))
|
|
||||||
require.NoError(t, os.WriteFile(input, []byte("numlockon=1\n"), 0o644))
|
|
||||||
|
|
||||||
changed, err := Snake.Migrate(dir, false)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, []string{layout, input}, changed)
|
|
||||||
data, _ := os.ReadFile(main)
|
|
||||||
assert.Equal(t, "exec-once=dms run\n", string(data))
|
|
||||||
data, _ = os.ReadFile(input)
|
|
||||||
assert.Equal(t, "numlock_on=1\n", string(data))
|
|
||||||
|
|
||||||
changed, err = Snake.Migrate(dir, true)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, []string{main}, changed)
|
|
||||||
backups, _ := filepath.Glob(main + ".backup.*")
|
|
||||||
assert.Len(t, backups, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMigrateReportsUnwritableFragmentAndContinues(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "dms"), 0o755))
|
|
||||||
binds := filepath.Join(dir, "dms", "binds.conf")
|
|
||||||
layout := filepath.Join(dir, "dms", "layout.conf")
|
|
||||||
require.NoError(t, os.WriteFile(binds, []byte("exec-once=foo\n"), 0o444))
|
|
||||||
require.NoError(t, os.WriteFile(layout, []byte("borderpx=2\n"), 0o644))
|
|
||||||
|
|
||||||
changed, err := Snake.Migrate(dir, false)
|
|
||||||
assert.ErrorContains(t, err, "binds.conf")
|
|
||||||
assert.Equal(t, []string{layout}, changed)
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package mangoconf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DMS-owned fragments under mango/dms; config.conf is the user's and only
|
|
||||||
// migrated on request.
|
|
||||||
var fragments = []string{"binds.conf", "colors.conf", "layout.conf", "cursor.conf", "input.conf", "outputs.conf", "windowrules.conf"}
|
|
||||||
|
|
||||||
// Migrate respells the dms fragments, and config.conf (after a backup) when includeMain is set.
|
|
||||||
// It returns the files rewritten; an unwritable file is reported without stopping the others.
|
|
||||||
func (d Dialect) Migrate(mangoDir string, includeMain bool) ([]string, error) {
|
|
||||||
var changed []string
|
|
||||||
var errs []error
|
|
||||||
paths := make([]string, 0, len(fragments)+1)
|
|
||||||
for _, f := range fragments {
|
|
||||||
paths = append(paths, filepath.Join(mangoDir, "dms", f))
|
|
||||||
}
|
|
||||||
if includeMain {
|
|
||||||
paths = append(paths, filepath.Join(mangoDir, "config.conf"))
|
|
||||||
}
|
|
||||||
for _, path := range paths {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
translated := d.Translate(string(data))
|
|
||||||
if translated == string(data) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if filepath.Base(path) == "config.conf" {
|
|
||||||
backup := path + ".backup." + time.Now().Format("2006-01-02_15-04-05")
|
|
||||||
if err := os.WriteFile(backup, data, 0o644); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("backup %s: %w", path, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte(translated), 0o644); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("write %s: %w", path, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
changed = append(changed, path)
|
|
||||||
}
|
|
||||||
return changed, errors.Join(errs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Dialect) String() string {
|
|
||||||
if d == Snake {
|
|
||||||
return "snake"
|
|
||||||
}
|
|
||||||
return "legacy"
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
|
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/dank16"
|
"github.com/duxlabs-dev/dux-shell/core/internal/dank16"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
|
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/lucasb-eyer/go-colorful"
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
@@ -44,7 +43,6 @@ const (
|
|||||||
TemplateKindGTK
|
TemplateKindGTK
|
||||||
TemplateKindVSCode
|
TemplateKindVSCode
|
||||||
TemplateKindEmacs
|
TemplateKindEmacs
|
||||||
TemplateKindMango
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateDef struct {
|
type TemplateDef struct {
|
||||||
@@ -63,7 +61,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true},
|
{ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true},
|
||||||
{ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"},
|
{ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"},
|
||||||
{ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"},
|
{ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"},
|
||||||
{ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml", RequiredEnv: "MANGO_INSTANCE_SIGNATURE", Kind: TemplateKindMango},
|
{ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml", RequiredEnv: "MANGO_INSTANCE_SIGNATURE"},
|
||||||
{ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"},
|
{ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"},
|
||||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
||||||
{ID: "fcitx5", Commands: []string{"fcitx5"}, ConfigDirs: []string{"fcitx5"}, ConfigFile: "fcitx5.toml"},
|
{ID: "fcitx5", Commands: []string{"fcitx5"}, ConfigDirs: []string{"fcitx5"}, ConfigFile: "fcitx5.toml"},
|
||||||
@@ -725,12 +723,6 @@ output_path = '%s'
|
|||||||
for _, editor := range vscodeEditors {
|
for _, editor := range vscodeEditors {
|
||||||
appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir)
|
||||||
}
|
}
|
||||||
case TemplateKindMango:
|
|
||||||
configFile := tmpl.ConfigFile
|
|
||||||
if mangoconf.Detect() == mangoconf.Snake {
|
|
||||||
configFile = "mangowc-snake.toml"
|
|
||||||
}
|
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, configFile)
|
|
||||||
case TemplateKindEmacs:
|
case TemplateKindEmacs:
|
||||||
if utils.EmacsConfigDir() != "" {
|
if utils.EmacsConfigDir() != "" {
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
|
||||||
@@ -965,6 +957,7 @@ var vscodeEditors = []vscodeEditor{
|
|||||||
{"cursor", ".cursor"},
|
{"cursor", ".cursor"},
|
||||||
{"windsurf", ".windsurf"},
|
{"windsurf", ".windsurf"},
|
||||||
{"positron", ".positron"},
|
{"positron", ".positron"},
|
||||||
|
{"antigravity", ".antigravity-ide"},
|
||||||
{"vscode-insiders", ".vscode-insiders"},
|
{"vscode-insiders", ".vscode-insiders"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
|||||||
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
|
func TestBuildMergedConfigMangoSession(t *testing.T) {
|
||||||
t.Setenv("MANGO_INSTANCE_SIGNATURE", "")
|
t.Setenv("MANGO_INSTANCE_SIGNATURE", "")
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
@@ -555,6 +555,31 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
|
|||||||
t.Fatalf("failed to read merged config: %v", err)
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
}
|
}
|
||||||
assert.NotContains(t, string(output), "[templates.dmsmango]")
|
assert.NotContains(t, string(output), "[templates.dmsmango]")
|
||||||
|
|
||||||
|
socket := filepath.Join(tempDir, "mango.sock")
|
||||||
|
if err := os.WriteFile(socket, nil, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("MANGO_INSTANCE_SIGNATURE", socket)
|
||||||
|
checker := mocks_utils.NewMockAppChecker(t)
|
||||||
|
checker.EXPECT().AnyCommandExists("mango").Return(true)
|
||||||
|
opts.AppChecker = checker
|
||||||
|
cfgFile, err = os.CreateTemp(tempDir, "merged-active-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer cfgFile.Close()
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output, err = os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Contains(t, string(output), mangowcConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppendConfigConfigDirExists(t *testing.T) {
|
func TestAppendConfigConfigDirExists(t *testing.T) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
berrors "go.etcd.io/bbolt/errors"
|
||||||
|
|
||||||
wlclient "github.com/duxlabs-dev/dux-go/wayland/client"
|
wlclient "github.com/duxlabs-dev/dux-go/wayland/client"
|
||||||
"github.com/duxlabs-dev/dux-go/wayland/ext_data_control"
|
"github.com/duxlabs-dev/dux-go/wayland/ext_data_control"
|
||||||
@@ -55,8 +56,14 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
|
|
||||||
configPath, _ := getConfigPath()
|
configPath, _ := getConfigPath()
|
||||||
|
|
||||||
|
db, err := openDB(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
config: config,
|
config: config,
|
||||||
|
db: db,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
display: display,
|
display: display,
|
||||||
wlCtx: wlCtx,
|
wlCtx: wlCtx,
|
||||||
@@ -69,6 +76,7 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
|
|
||||||
if !config.Disabled {
|
if !config.Disabled {
|
||||||
if err := m.setupRegistry(); err != nil {
|
if err := m.setupRegistry(); err != nil {
|
||||||
|
db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,12 +86,6 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
|||||||
|
|
||||||
go m.watchConfig()
|
go m.watchConfig()
|
||||||
|
|
||||||
db, err := openDB(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open db: %w", err)
|
|
||||||
}
|
|
||||||
m.db = db
|
|
||||||
|
|
||||||
if err := m.migrateHashes(); err != nil {
|
if err := m.migrateHashes(); err != nil {
|
||||||
log.Errorf("Failed to migrate hashes: %v", err)
|
log.Errorf("Failed to migrate hashes: %v", err)
|
||||||
}
|
}
|
||||||
@@ -117,6 +119,9 @@ func openDB(path string) (*bolt.DB, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, berrors.ErrTimeout) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
log.Errorf("Clipboard db corrupted, salvaging readable entries: %v", err)
|
log.Errorf("Clipboard db corrupted, salvaging readable entries: %v", err)
|
||||||
|
|
||||||
|
|||||||
@@ -887,6 +887,24 @@ func TestOpenDB_HealsGarbageFile(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenDB_LockedByAnotherProcessKeepsHistory(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "db")
|
||||||
|
holder, err := openDB(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
m := &Manager{config: DefaultConfig(), db: holder, dbPath: path}
|
||||||
|
require.NoError(t, m.storeEntry(Entry{Data: []byte("x"), MimeType: "text/plain"}))
|
||||||
|
|
||||||
|
_, err = openDB(path)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.NoError(t, holder.Close())
|
||||||
|
|
||||||
|
reopened, err := openDB(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer reopened.Close()
|
||||||
|
m = &Manager{config: DefaultConfig(), db: reopened, dbPath: path}
|
||||||
|
assert.Len(t, m.GetHistory(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStoreEntry_CorruptDBReturnsErrorNotPanic(t *testing.T) {
|
func TestStoreEntry_CorruptDBReturnsErrorNotPanic(t *testing.T) {
|
||||||
for _, tt := range corruptDBCases {
|
for _, tt := range corruptDBCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewManager() (*Manager, error) {
|
func NewManager(keepLockOnUnlock bool) (*Manager, error) {
|
||||||
conn, err := dbus.SystemBus()
|
conn, err := dbus.SystemBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
@@ -25,6 +25,8 @@ func NewManager() (*Manager, error) {
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
signals: make(chan *dbus.Signal, 256),
|
signals: make(chan *dbus.Signal, 256),
|
||||||
|
|
||||||
|
keepLockOnUnlock: keepLockOnUnlock,
|
||||||
}
|
}
|
||||||
m.sleepInhibitorEnabled.Store(true)
|
m.sleepInhibitorEnabled.Store(true)
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,22 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case dbusSessionInterface + ".Unlock":
|
case dbusSessionInterface + ".Unlock":
|
||||||
lockedHint := m.sessionLockedHint()
|
if m.keepLockOnUnlock {
|
||||||
m.stateMutex.Lock()
|
// Only the locker clears LockedHint, so take both from logind, and
|
||||||
m.state.Locked = false
|
// keep the cached state when logind cannot be read.
|
||||||
m.state.LockedHint = lockedHint
|
if m.sessionObj != nil && m.updateSessionState() == nil && !m.snapshotState().Locked {
|
||||||
m.stateMutex.Unlock()
|
m.prelockedReady.Store(false)
|
||||||
m.notifySubscribers()
|
}
|
||||||
m.prelockedReady.Store(false)
|
m.notifySubscribers()
|
||||||
|
} else {
|
||||||
|
lockedHint := m.sessionLockedHint()
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Locked = false
|
||||||
|
m.state.LockedHint = lockedHint
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
m.notifySubscribers()
|
||||||
|
m.prelockedReady.Store(false)
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel the lock timer if it's still running
|
// Cancel the lock timer if it's still running
|
||||||
m.lockTimerMu.Lock()
|
m.lockTimerMu.Lock()
|
||||||
|
|||||||
@@ -37,42 +37,58 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) {
|
|||||||
|
|
||||||
func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
|
func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
lockedHint bool
|
keepLock bool
|
||||||
|
call *dbus.Call // logind's GetAll answer; nil means no session object
|
||||||
|
wantLocked bool
|
||||||
|
wantLockedHint bool
|
||||||
}{
|
}{
|
||||||
{name: "logind cleared LockedHint", lockedHint: false},
|
{name: "logind cleared LockedHint", call: lockedHintCall(false)},
|
||||||
{name: "logind still reports LockedHint", lockedHint: true},
|
{name: "logind still reports LockedHint", call: lockedHintCall(true), wantLockedHint: true},
|
||||||
|
{name: "logind unreachable", call: &dbus.Call{Err: assert.AnError}},
|
||||||
|
{name: "policy, LockedHint still set", keepLock: true, call: lockedHintCall(true), wantLocked: true, wantLockedHint: true},
|
||||||
|
{name: "policy, LockedHint cleared", keepLock: true, call: lockedHintCall(false)},
|
||||||
|
{name: "policy, logind unreachable", keepLock: true, call: &dbus.Call{Err: assert.AnError}, wantLocked: true, wantLockedHint: true},
|
||||||
|
{name: "policy, no session object", keepLock: true, wantLocked: true, wantLockedHint: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
sessionObj := mockdbus.NewMockBusObject(t)
|
|
||||||
sessionObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.login1.Session").Return(&dbus.Call{
|
|
||||||
Body: []any{map[string]dbus.Variant{"LockedHint": dbus.MakeVariant(tt.lockedHint)}},
|
|
||||||
})
|
|
||||||
|
|
||||||
manager := &Manager{
|
manager := &Manager{
|
||||||
state: &SessionState{
|
state: &SessionState{
|
||||||
Locked: true,
|
Locked: true,
|
||||||
LockedHint: true,
|
LockedHint: true,
|
||||||
},
|
},
|
||||||
stateMutex: sync.RWMutex{},
|
stateMutex: sync.RWMutex{},
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
sessionObj: sessionObj,
|
keepLockOnUnlock: tt.keepLock,
|
||||||
}
|
}
|
||||||
|
if tt.call != nil {
|
||||||
|
sessionObj := mockdbus.NewMockBusObject(t)
|
||||||
|
sessionObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.login1.Session").Return(tt.call)
|
||||||
|
manager.sessionObj = sessionObj
|
||||||
|
}
|
||||||
|
manager.markLockerReady()
|
||||||
|
|
||||||
manager.handleDBusSignal(&dbus.Signal{
|
manager.handleDBusSignal(&dbus.Signal{
|
||||||
Name: "org.freedesktop.login1.Session.Unlock",
|
Name: "org.freedesktop.login1.Session.Unlock",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, tt.wantLocked, manager.prelockedReady.Load())
|
||||||
manager.stateMutex.RLock()
|
manager.stateMutex.RLock()
|
||||||
defer manager.stateMutex.RUnlock()
|
defer manager.stateMutex.RUnlock()
|
||||||
assert.False(t, manager.state.Locked)
|
assert.Equal(t, tt.wantLocked, manager.state.Locked)
|
||||||
assert.Equal(t, tt.lockedHint, manager.state.LockedHint)
|
assert.Equal(t, tt.wantLockedHint, manager.state.LockedHint)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lockedHintCall(lockedHint bool) *dbus.Call {
|
||||||
|
return &dbus.Call{Body: []any{map[string]dbus.Variant{
|
||||||
|
"LockedHint": dbus.MakeVariant(lockedHint),
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
|
func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
|
||||||
t.Run("preparing for sleep - true", func(t *testing.T) {
|
t.Run("preparing for sleep - true", func(t *testing.T) {
|
||||||
manager := &Manager{
|
manager := &Manager{
|
||||||
|
|||||||
@@ -69,4 +69,5 @@ type Manager struct {
|
|||||||
lockTimer *time.Timer
|
lockTimer *time.Timer
|
||||||
sleepInhibitorEnabled atomic.Bool
|
sleepInhibitorEnabled atomic.Bool
|
||||||
fallbackDelay time.Duration
|
fallbackDelay time.Duration
|
||||||
|
keepLockOnUnlock bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/duxlabs-dev/dux-go/ipc"
|
"github.com/duxlabs-dev/dux-go/ipc"
|
||||||
"github.com/duxlabs-dev/dux-go/syncmap"
|
"github.com/duxlabs-dev/dux-go/syncmap"
|
||||||
|
"github.com/duxlabs-dev/dux-shell/core/internal/clipolicy"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/geolocation"
|
"github.com/duxlabs-dev/dux-shell/core/internal/geolocation"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/matugen"
|
"github.com/duxlabs-dev/dux-shell/core/internal/matugen"
|
||||||
@@ -37,6 +38,7 @@ import (
|
|||||||
"github.com/duxlabs-dev/dux-shell/core/internal/server/wlroutput"
|
"github.com/duxlabs-dev/dux-shell/core/internal/server/wlroutput"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/systemd"
|
"github.com/duxlabs-dev/dux-shell/core/internal/systemd"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
const APIVersion = 38
|
const APIVersion = 38
|
||||||
@@ -108,7 +110,12 @@ func InitializeNetworkManager() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InitializeLoginctlManager() error {
|
func InitializeLoginctlManager() error {
|
||||||
manager, err := loginctl.NewManager()
|
keepLockOnUnlock, err := clipolicy.LogindUnlockDisabled(afero.NewOsFs())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("CLI policy unreadable, keeping locks through logind Unlock: %v", err)
|
||||||
|
keepLockOnUnlock = true
|
||||||
|
}
|
||||||
|
manager, err := loginctl.NewManager(keepLockOnUnlock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to initialize loginctl manager: %v", err)
|
log.Warnf("Failed to initialize loginctl manager: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -194,3 +194,24 @@ func TestAlertRecordSurvivesRestart(t *testing.T) {
|
|||||||
assert.Nil(t, s.Limit, "the alert already fired before the restart")
|
assert.Nil(t, s.Limit, "the alert already fired before the restart")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenDBLockedByAnotherProcessKeepsData(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "db")
|
||||||
|
c := &clock{now: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)}
|
||||||
|
now := func() time.Time { return c.now }
|
||||||
|
|
||||||
|
m, err := newManager(path, now)
|
||||||
|
require.NoError(t, err)
|
||||||
|
m.SetState("firefox", true, 1)
|
||||||
|
c.advance(10 * time.Second)
|
||||||
|
m.Flush()
|
||||||
|
|
||||||
|
_, err = newManager(path, now)
|
||||||
|
require.Error(t, err)
|
||||||
|
m.Close()
|
||||||
|
|
||||||
|
m, err = newManager(path, now)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer m.Close()
|
||||||
|
assert.Equal(t, int64(10), m.Summary(1)[0].Active)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package wellbeing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
"github.com/duxlabs-dev/dux-shell/core/internal/log"
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
"github.com/duxlabs-dev/dux-shell/core/internal/utils"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
|
berrors "go.etcd.io/bbolt/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -31,6 +33,9 @@ func openDB(path string) (*bolt.DB, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, berrors.ErrTimeout) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
log.Errorf("Wellbeing db unreadable, starting over: %v", err)
|
log.Errorf("Wellbeing db unreadable, starting over: %v", err)
|
||||||
corruptPath := path + ".corrupt"
|
corruptPath := path + ".corrupt"
|
||||||
os.Remove(corruptPath)
|
os.Remove(corruptPath)
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func (m Model) configReplacementNote() string {
|
|||||||
if m.useSystemdConfig() && mangoconf.SessionTargetInstalled() {
|
if m.useSystemdConfig() && mangoconf.SessionTargetInstalled() {
|
||||||
return "Replacing Mango writes the DMS Mango template and starts the user systemd dms service with mango-session.target. Existing binds, monitor and window rules move into the dms/ files."
|
return "Replacing Mango writes the DMS Mango template and starts the user systemd dms service with mango-session.target. Existing binds, monitor and window rules move into the dms/ files."
|
||||||
}
|
}
|
||||||
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with " + mangoconf.Detect().Key("exec_once") + "=dms run. Existing binds, monitor and window rules move into the dms/ files."
|
return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec_once=dms run. Existing binds, monitor and window rules move into the dms/ files."
|
||||||
case config.BindsConfigType:
|
case config.BindsConfigType:
|
||||||
return "Keep uses your binds as they are; replace starts from the DMS stock binds. A timestamped backup is kept either way."
|
return "Keep uses your binds as they are; replace starts from the DMS stock binds. A timestamped backup is kept either way."
|
||||||
case "Ghostty":
|
case "Ghostty":
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func (m Model) troubleshootingHints() (autostart, logs string) {
|
|||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
autostart = `remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`
|
autostart = `remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua`
|
||||||
default:
|
default:
|
||||||
autostart = "remove '" + mangoconf.Detect().Key("exec_once") + "=dms run' from ~/.config/mango/config.conf"
|
autostart = "remove 'exec_once=dms run' from ~/.config/mango/config.conf"
|
||||||
}
|
}
|
||||||
return autostart, logs
|
return autostart, logs
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ func (m Model) troubleshootingHints() (autostart, logs string) {
|
|||||||
if mangoconf.SessionTargetInstalled() {
|
if mangoconf.SessionTargetInstalled() {
|
||||||
return "rm ~/.config/systemd/user/mango-session.target.wants/dms.service", "journalctl --user -u dms"
|
return "rm ~/.config/systemd/user/mango-session.target.wants/dms.service", "journalctl --user -u dms"
|
||||||
}
|
}
|
||||||
return "remove '" + mangoconf.Detect().Key("exec_once") + "=dms run' from ~/.config/mango/config.conf", "qs -p ~/.config/quickshell/dms log"
|
return "remove 'exec_once=dms run' from ~/.config/mango/config.conf", "qs -p ~/.config/quickshell/dms log"
|
||||||
}
|
}
|
||||||
return "systemctl --user disable dms", "journalctl --user -u dms"
|
return "systemctl --user disable dms", "journalctl --user -u dms"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,17 @@ import (
|
|||||||
"github.com/duxlabs-dev/dux-shell/core/internal/windowrules"
|
"github.com/duxlabs-dev/dux-shell/core/internal/windowrules"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mango window rules are flat `windowrule=key:value,...` lines (`window_rule=`
|
// DMS-managed rules live in dms/windowrules.conf (sourced from config.conf),
|
||||||
// with snake_case fields after the upstream rename; both are read). DMS-managed rules
|
// each preceded by an `# @id=<id> @name=<name>` comment so they round-trip.
|
||||||
// live in dms/windowrules.conf (sourced from config.conf), each preceded by an
|
|
||||||
// `# @id=<id> @name=<name>` comment so they round-trip.
|
|
||||||
|
|
||||||
type MangoWindowRule struct {
|
type MangoWindowRule struct {
|
||||||
Source string
|
Source string
|
||||||
Fields map[string]string
|
Fields map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
var mangoWindowRuleRegex = regexp.MustCompile(`^(?:windowrule|window_rule)\s*=\s*(.+)$`)
|
var mangoWindowRuleRegex = regexp.MustCompile(`^window_rule\s*=\s*(.+)$`)
|
||||||
var mangoMetaCommentRegex = regexp.MustCompile(`^#\s*@id=(\S*)\s*@name=(.*)$`)
|
var mangoMetaCommentRegex = regexp.MustCompile(`^#\s*@id=(\S*)\s*@name=(.*)$`)
|
||||||
var mangoOnceRuleRegex = regexp.MustCompile(`^(?:windowrule-once|window_rule_once)\s*=`)
|
var mangoOnceRuleRegex = regexp.MustCompile(`^window_rule_once\s*=`)
|
||||||
|
|
||||||
// Fields the editor models; anything else on a DMS rule line is carried through saves.
|
// Fields the editor models; anything else on a DMS rule line is carried through saves.
|
||||||
var mangoModeledFields = map[string]bool{
|
var mangoModeledFields = map[string]bool{
|
||||||
@@ -42,7 +40,7 @@ func mangoUnmodeledFields(value string) [][2]string {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := mangoconf.Normalize(strings.TrimSpace(k))
|
key := strings.TrimSpace(k)
|
||||||
sized := (key == "width" || key == "height") && (!hasWidth || !hasHeight)
|
sized := (key == "width" || key == "height") && (!hasWidth || !hasHeight)
|
||||||
if key != "" && (!mangoModeledFields[key] || sized) {
|
if key != "" && (!mangoModeledFields[key] || sized) {
|
||||||
extra = append(extra, [2]string{key, strings.TrimSpace(v)})
|
extra = append(extra, [2]string{key, strings.TrimSpace(v)})
|
||||||
@@ -62,7 +60,7 @@ func parseMangoWindowRuleLine(value string) map[string]string {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := mangoconf.Normalize(strings.TrimSpace(before))
|
key := strings.TrimSpace(before)
|
||||||
val := strings.TrimSpace(after)
|
val := strings.TrimSpace(after)
|
||||||
if key != "" {
|
if key != "" {
|
||||||
fields[key] = val
|
fields[key] = val
|
||||||
@@ -89,7 +87,7 @@ func mangoOverridePath(configDir string) string {
|
|||||||
return filepath.Join(configDir, "dms", "windowrules.conf")
|
return filepath.Join(configDir, "dms", "windowrules.conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseMangoRulesFile reads a config file and returns its windowrule= lines.
|
// parseMangoRulesFile reads a config file and returns its window_rule= lines.
|
||||||
func parseMangoRulesFile(path, source string) []MangoWindowRule {
|
func parseMangoRulesFile(path, source string) []MangoWindowRule {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -215,11 +213,11 @@ func ConvertMangoRulesToWindowRules(mangoRules []MangoWindowRule) []windowrules.
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, extra ...[2]string) string {
|
func formatMangoRule(rule windowrules.WindowRule, extra ...[2]string) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
add := func(k, v string) {
|
add := func(k, v string) {
|
||||||
if v != "" {
|
if v != "" {
|
||||||
parts = append(parts, dialect.Key(k)+":"+v)
|
parts = append(parts, k+":"+v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +233,7 @@ func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, ext
|
|||||||
|
|
||||||
addBool := func(k string, b *bool) {
|
addBool := func(k string, b *bool) {
|
||||||
if b != nil {
|
if b != nil {
|
||||||
parts = append(parts, dialect.Key(k)+":"+mangoBoolStr(b))
|
parts = append(parts, k+":"+mangoBoolStr(b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addBool("is_floating", rule.Actions.OpenFloating)
|
addBool("is_floating", rule.Actions.OpenFloating)
|
||||||
@@ -249,19 +247,18 @@ func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, ext
|
|||||||
add(kv[0], kv[1])
|
add(kv[0], kv[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialect.Key("window_rule") + "=" + strings.Join(parts, ",")
|
return "window_rule=" + strings.Join(parts, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
type MangoWritableProvider struct {
|
type MangoWritableProvider struct {
|
||||||
configDir string
|
configDir string
|
||||||
dialect mangoconf.Dialect
|
|
||||||
// Filled by LoadDMSRules so the following write keeps what the editor cannot show.
|
// Filled by LoadDMSRules so the following write keeps what the editor cannot show.
|
||||||
extraFields map[string][][2]string
|
extraFields map[string][][2]string
|
||||||
onceLines []string
|
onceLines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMangoWritableProvider(configDir string) *MangoWritableProvider {
|
func NewMangoWritableProvider(configDir string) *MangoWritableProvider {
|
||||||
return &MangoWritableProvider{configDir: configDir, dialect: mangoconf.Detect()}
|
return &MangoWritableProvider{configDir: configDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MangoWritableProvider) Name() string { return "mango" }
|
func (p *MangoWritableProvider) Name() string { return "mango" }
|
||||||
@@ -360,7 +357,7 @@ func (p *MangoWritableProvider) WriteDMSRules(rules []windowrules.WindowRule) er
|
|||||||
if id == "" {
|
if id == "" {
|
||||||
id = fmt.Sprintf("rule_%d", i)
|
id = fmt.Sprintf("rule_%d", i)
|
||||||
}
|
}
|
||||||
line := formatMangoRule(r, p.dialect, p.extraFields[r.ID]...)
|
line := formatMangoRule(r, p.extraFields[r.ID]...)
|
||||||
if err := mangoconf.CheckLine(line); err != nil {
|
if err := mangoconf.CheckLine(line); err != nil {
|
||||||
return fmt.Errorf("rule %q: %w", r.Name, err)
|
return fmt.Errorf("rule %q: %w", r.Name, err)
|
||||||
}
|
}
|
||||||
@@ -369,7 +366,7 @@ func (p *MangoWritableProvider) WriteDMSRules(rules []windowrules.WindowRule) er
|
|||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
for _, line := range p.onceLines {
|
for _, line := range p.onceLines {
|
||||||
sb.WriteString(p.dialect.Translate(line))
|
sb.WriteString(line)
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/mangoconf"
|
|
||||||
"github.com/duxlabs-dev/dux-shell/core/internal/windowrules"
|
"github.com/duxlabs-dev/dux-shell/core/internal/windowrules"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseMangoWindowRuleLineReadsBothDialects(t *testing.T) {
|
func TestParseMangoWindowRuleLineSplitsFields(t *testing.T) {
|
||||||
for _, line := range []string{
|
for _, line := range []string{
|
||||||
"appid:firefox,title:Gmail,isfloating:1,tags:2,monitor:HDMI-A-1",
|
|
||||||
"app_id:firefox,title:Gmail,is_floating:1,tags:2,monitor:HDMI-A-1",
|
"app_id:firefox,title:Gmail,is_floating:1,tags:2,monitor:HDMI-A-1",
|
||||||
|
"app_id: firefox , title:Gmail,is_floating:1,tags:2,monitor:HDMI-A-1",
|
||||||
} {
|
} {
|
||||||
fields := parseMangoWindowRuleLine(line)
|
fields := parseMangoWindowRuleLine(line)
|
||||||
want := map[string]string{"app_id": "firefox", "title": "Gmail", "is_floating": "1", "tags": "2", "monitor": "HDMI-A-1"}
|
want := map[string]string{"app_id": "firefox", "title": "Gmail", "is_floating": "1", "tags": "2", "monitor": "HDMI-A-1"}
|
||||||
@@ -25,13 +24,10 @@ func TestParseMangoWindowRuleLineReadsBothDialects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMangoRulesWrittenInInstalledDialect(t *testing.T) {
|
func TestMangoRulesWrittenInSnakeCase(t *testing.T) {
|
||||||
floating := true
|
floating := true
|
||||||
rule := windowrules.WindowRule{MatchCriteria: windowrules.MatchCriteria{AppID: "discord"}, Actions: windowrules.Actions{OpenFloating: &floating, NoAnim: &floating}}
|
rule := windowrules.WindowRule{MatchCriteria: windowrules.MatchCriteria{AppID: "discord"}, Actions: windowrules.Actions{OpenFloating: &floating, NoAnim: &floating}}
|
||||||
if got, want := formatMangoRule(rule, mangoconf.Legacy), "windowrule=appid:discord,isfloating:1,isnoanimation:1"; got != want {
|
if got, want := formatMangoRule(rule), "window_rule=app_id:discord,is_floating:1,no_animation:1"; got != want {
|
||||||
t.Errorf("legacy = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
if got, want := formatMangoRule(rule, mangoconf.Snake), "window_rule=app_id:discord,is_floating:1,no_animation:1"; got != want {
|
|
||||||
t.Errorf("snake = %q, want %q", got, want)
|
t.Errorf("snake = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +35,7 @@ func TestMangoRulesWrittenInInstalledDialect(t *testing.T) {
|
|||||||
rulesPath := filepath.Join(dir, "dms", "windowrules.conf")
|
rulesPath := filepath.Join(dir, "dms", "windowrules.conf")
|
||||||
_ = os.MkdirAll(filepath.Dir(rulesPath), 0o755)
|
_ = os.MkdirAll(filepath.Dir(rulesPath), 0o755)
|
||||||
_ = os.WriteFile(rulesPath, []byte("# @id=a @name=A\nwindow_rule=app_id:discord,is_floating:1\n"), 0o644)
|
_ = os.WriteFile(rulesPath, []byte("# @id=a @name=A\nwindow_rule=app_id:discord,is_floating:1\n"), 0o644)
|
||||||
loaded, err := (&MangoWritableProvider{configDir: dir, dialect: mangoconf.Snake}).LoadDMSRules()
|
loaded, err := (&MangoWritableProvider{configDir: dir}).LoadDMSRules()
|
||||||
if err != nil || len(loaded) != 1 || loaded[0].MatchCriteria.AppID != "discord" || loaded[0].Actions.OpenFloating == nil {
|
if err != nil || len(loaded) != 1 || loaded[0].MatchCriteria.AppID != "discord" || loaded[0].Actions.OpenFloating == nil {
|
||||||
t.Fatalf("snake rule not loaded: %+v, %v", loaded, err)
|
t.Fatalf("snake rule not loaded: %+v, %v", loaded, err)
|
||||||
}
|
}
|
||||||
@@ -47,7 +43,7 @@ func TestMangoRulesWrittenInInstalledDialect(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertMangoRulesToWindowRules(t *testing.T) {
|
func TestConvertMangoRulesToWindowRules(t *testing.T) {
|
||||||
mangoRules := []MangoWindowRule{
|
mangoRules := []MangoWindowRule{
|
||||||
{Source: "config.conf", Fields: parseMangoWindowRuleLine("appid:discord,tags:9,isfloating:1,noblur:1")},
|
{Source: "config.conf", Fields: parseMangoWindowRuleLine("app_id:discord,tags:9,is_floating:1,no_blur:1")},
|
||||||
}
|
}
|
||||||
rules := ConvertMangoRulesToWindowRules(mangoRules)
|
rules := ConvertMangoRulesToWindowRules(mangoRules)
|
||||||
if len(rules) != 1 {
|
if len(rules) != 1 {
|
||||||
@@ -141,9 +137,9 @@ func TestMangoRuleSaveKeepsUnmodeledFieldsAndOnceRules(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, "dms", "windowrules.conf")
|
path := filepath.Join(dir, "dms", "windowrules.conf")
|
||||||
_ = os.MkdirAll(filepath.Dir(path), 0o755)
|
_ = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||||
_ = os.WriteFile(path, []byte("# @id=a @name=A\nwindowrule=appid:mpv,isglobal:1,offsetx:20,width:0.5\nwindowrule-once=appid:firefox,tags:2\n"), 0o644)
|
_ = os.WriteFile(path, []byte("# @id=a @name=A\nwindow_rule=app_id:mpv,is_global:1,offset_x:20,width:0.5\nwindow_rule_once=app_id:firefox,tags:2\n"), 0o644)
|
||||||
|
|
||||||
p := &MangoWritableProvider{configDir: dir, dialect: mangoconf.Legacy}
|
p := &MangoWritableProvider{configDir: dir}
|
||||||
floating := true
|
floating := true
|
||||||
if err := p.SetRule(windowrules.WindowRule{ID: "b", Name: "B", MatchCriteria: windowrules.MatchCriteria{AppID: "foot"}, Actions: windowrules.Actions{OpenFloating: &floating}}); err != nil {
|
if err := p.SetRule(windowrules.WindowRule{ID: "b", Name: "B", MatchCriteria: windowrules.MatchCriteria{AppID: "foot"}, Actions: windowrules.Actions{OpenFloating: &floating}}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -151,9 +147,9 @@ func TestMangoRuleSaveKeepsUnmodeledFieldsAndOnceRules(t *testing.T) {
|
|||||||
data, _ := os.ReadFile(path)
|
data, _ := os.ReadFile(path)
|
||||||
got := string(data)
|
got := string(data)
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"windowrule=appid:mpv,isglobal:1,offsetx:20,width:0.5\n",
|
"window_rule=app_id:mpv,is_global:1,offset_x:20,width:0.5\n",
|
||||||
"windowrule=appid:foot,isfloating:1\n",
|
"window_rule=app_id:foot,is_floating:1\n",
|
||||||
"windowrule-once=appid:firefox,tags:2\n",
|
"window_rule_once=app_id:firefox,tags:2\n",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
t.Errorf("missing %q in:\n%s", want, got)
|
t.Errorf("missing %q in:\n%s", want, got)
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ core shell features.
|
|||||||
- **Users:** `pw(8)`.
|
- **Users:** `pw(8)`.
|
||||||
- **Package updates:** `pkg(8)`.
|
- **Package updates:** `pkg(8)`.
|
||||||
- **Compositor/socket discovery:** `sockstat(1)` where Linux would use `/proc`.
|
- **Compositor/socket discovery:** `sockstat(1)` where Linux would use `/proc`.
|
||||||
- **Greeter state:** FreeBSD rc service detection for greetd.
|
- **Sessions:** FreeBSD has no logind. DMS lists the current session only.
|
||||||
- **Sessions:** elogind/loginctl is used when installed; otherwise DMS exposes
|
|
||||||
the current session without repeatedly executing a missing Linux command.
|
|
||||||
|
|
||||||
## Recommended runtime tools
|
## Recommended runtime tools
|
||||||
|
|
||||||
@@ -30,13 +28,12 @@ Quickshell, grim/slurp, wl-clipboard, and a notification/audio stack as desired)
|
|||||||
from FreeBSD packages/ports. DMS's native audio path itself only needs the base
|
from FreeBSD packages/ports. DMS's native audio path itself only needs the base
|
||||||
system `mixer` utility.
|
system `mixer` utility.
|
||||||
|
|
||||||
For session switching and login-manager integration, install/use elogind so
|
|
||||||
`loginctl` is available. Without elogind, locking and the current-session shell
|
|
||||||
remain usable, but switching between already-running graphical sessions is not
|
|
||||||
provided by the base system.
|
|
||||||
|
|
||||||
## FreeBSD limitations that DMS should not fake
|
## FreeBSD limitations that DMS should not fake
|
||||||
|
|
||||||
|
- No logind, so the daemon's `org.freedesktop.login1` integration is off: no
|
||||||
|
lock or unlock requests from the session manager, no sleep inhibitor, and no
|
||||||
|
switching between running sessions (`dms switch-user`).
|
||||||
|
- Greeter status detection uses `systemctl` and reports greetd as not enabled.
|
||||||
- FreeBSD ACPI S4 hibernation is hardware/kernel dependent and is not presented
|
- FreeBSD ACPI S4 hibernation is hardware/kernel dependent and is not presented
|
||||||
as generally available by DMS.
|
as generally available by DMS.
|
||||||
- Charge thresholds are vendor-specific; the Linux `/sys/class/power_supply`
|
- Charge thresholds are vendor-specific; the Linux `/sys/class/power_supply`
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ include_package() {
|
|||||||
[[ "$PACKAGE_FILTER" == "all" || "$PACKAGE_FILTER" == "$package" ]]
|
[[ "$PACKAGE_FILTER" == "all" || "$PACKAGE_FILTER" == "$package" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
CURRENT_COMMIT="$(git rev-parse --short=8 HEAD)"
|
CURRENT_COMMIT="$(git rev-parse HEAD)"
|
||||||
LATEST_TAG=""
|
LATEST_TAG=""
|
||||||
TARGETS=()
|
TARGETS=()
|
||||||
|
|
||||||
@@ -118,9 +118,9 @@ for pkg_info in "${PACKAGES[@]}"; do
|
|||||||
reason="missing from ${series}"
|
reason="missing from ${series}"
|
||||||
elif [[ "$type" == "git" ]]; then
|
elif [[ "$type" == "git" ]]; then
|
||||||
ppa_commit="$(embedded_commit "$ppa_version")"
|
ppa_commit="$(embedded_commit "$ppa_version")"
|
||||||
if [[ "$ppa_commit" != "$CURRENT_COMMIT" ]]; then
|
if [[ -z "$ppa_commit" || "${CURRENT_COMMIT:0:${#ppa_commit}}" != "$ppa_commit" ]]; then
|
||||||
needs_update=true
|
needs_update=true
|
||||||
reason="commit ${ppa_commit:-none} -> ${CURRENT_COMMIT}"
|
reason="commit ${ppa_commit:-none} -> ${CURRENT_COMMIT:0:9}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -z "$LATEST_TAG" ]]; then
|
if [[ -z "$LATEST_TAG" ]]; then
|
||||||
|
|||||||
+1
-1
Submodule dux-qml-common updated: ebf5e6c002...802b4b127b
Generated
+3
-3
@@ -3,11 +3,11 @@
|
|||||||
"dank-qml-common": {
|
"dank-qml-common": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1792513666,
|
"lastModified": 1792694928,
|
||||||
"narHash": "sha256-RDqPdPrM5lnwRrfFGLz3IwgTjCV2Ie+nrTD3ldj1t/s=",
|
"narHash": "sha256-jb+Ns8c4kw2a9z9bes/9D+fWhG8pmD8NVurFbRMk7G4=",
|
||||||
"owner": "duxlabs-dev",
|
"owner": "duxlabs-dev",
|
||||||
"repo": "dux-qml-common",
|
"repo": "dux-qml-common",
|
||||||
"rev": "ebf5e6c002da1bdaa4aaa2f319fea7b8507abc42",
|
"rev": "802b4b127b482d85921e464ccc7cd629294b47e3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
inherit version;
|
inherit version;
|
||||||
pname = "dms-shell";
|
pname = "dms-shell";
|
||||||
src = ./core;
|
src = ./core;
|
||||||
vendorHash = "sha256-fN7v7DuWaqhtD28cTr0FZUIyW7nUqhQoVuatsL2fuo4=";
|
vendorHash = "sha256-sKkafJm4PkV/gCdcwdmHVMTnlv8EO9hDk5YDxHwt9/E=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
// cli-policy.json switches, set by `dms run` on each launch (core/internal/clipolicy).
|
||||||
|
Singleton {
|
||||||
|
readonly property bool disableLockIpcUnlock: Quickshell.env("DMS_POLICY_DISABLE_LOCK_IPC_UNLOCK") === "1"
|
||||||
|
readonly property bool disableLogindUnlock: Quickshell.env("DMS_POLICY_DISABLE_LOGIND_UNLOCK") === "1"
|
||||||
|
readonly property bool disableCustomLockPam: Quickshell.env("DMS_POLICY_DISABLE_CUSTOM_LOCK_PAM") === "1"
|
||||||
|
readonly property bool disableUserPlugins: Quickshell.env("DMS_POLICY_DISABLE_USER_PLUGINS") === "1"
|
||||||
|
readonly property bool disableSettingsIpcSet: Quickshell.env("DMS_POLICY_DISABLE_SETTINGS_IPC_SET") === "1"
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int cacheConfigVersion: 3
|
readonly property int cacheConfigVersion: 3
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericCacheLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -52,8 +50,6 @@ Singleton {
|
|||||||
property var fileBrowserSettings: ({})
|
property var fileBrowserSettings: ({})
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
loadCache();
|
loadCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +276,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: launcherCacheFile
|
id: launcherCacheFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : _stateDir + "/dms/launcher_cache.json"
|
path: _stateDir + "/dms/launcher_cache.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
@@ -290,19 +286,15 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: cacheFile
|
id: cacheFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : _stateDir + "/dms/cache.json"
|
path: _stateDir + "/dms/cache.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
parseCache(cacheFile.text());
|
parseCache(cacheFile.text());
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
log.info("No cache file found, starting fresh");
|
log.info("No cache file found, starting fresh");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ function parseMangoOutputs(content) {
|
|||||||
const result = {};
|
const result = {};
|
||||||
for (const line of content.split("\n")) {
|
for (const line of content.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
const match = trimmed.match(/^(?:monitorrule|monitor_rule)\s*=\s*(.*)$/);
|
const match = trimmed.match(/^monitor_rule\s*=\s*(.*)$/);
|
||||||
if (!match)
|
if (!match)
|
||||||
continue;
|
continue;
|
||||||
const entry = parseMangoRule(match[1]);
|
const entry = parseMangoRule(match[1]);
|
||||||
@@ -445,10 +445,10 @@ function mangoRuleFields(rule) {
|
|||||||
|
|
||||||
// Mango applies only the first matching monitor rule and resets hdr/icc/disable a rule omits,
|
// Mango applies only the first matching monitor rule and resets hdr/icc/disable a rule omits,
|
||||||
// so unmodeled fields of every existing rule for an output are carried into DMS's rule, first one winning.
|
// so unmodeled fields of every existing rule for an output are carried into DMS's rule, first one winning.
|
||||||
function mangoMonitorRuleLines(outputsData, existingText, snakeKeys) {
|
function mangoMonitorRuleLines(outputsData, existingText) {
|
||||||
const carried = {};
|
const carried = {};
|
||||||
for (const line of (existingText || "").split("\n")) {
|
for (const line of (existingText || "").split("\n")) {
|
||||||
const match = line.trim().match(/^(?:monitorrule|monitor_rule)\s*=\s*(.*)$/);
|
const match = line.trim().match(/^monitor_rule\s*=\s*(.*)$/);
|
||||||
if (!match)
|
if (!match)
|
||||||
continue;
|
continue;
|
||||||
const fields = mangoRuleFields(match[1]);
|
const fields = mangoRuleFields(match[1]);
|
||||||
@@ -496,7 +496,7 @@ function mangoMonitorRuleLines(outputsData, existingText, snakeKeys) {
|
|||||||
}
|
}
|
||||||
if (output.enabled === false)
|
if (output.enabled === false)
|
||||||
fields.push("disable:1");
|
fields.push("disable:1");
|
||||||
lines.push((snakeKeys ? "monitor_rule=" : "monitorrule=") + fields.join(","));
|
lines.push("monitor_rule=" + fields.join(","));
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,16 @@ function physicalSize(output) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roundLogical(value, compositor) {
|
||||||
|
switch (compositor) {
|
||||||
|
case "niri":
|
||||||
|
// niri rounds the placed output, smithay's Space ceils mapped ones, only ceil never overlaps in either order (#3875)
|
||||||
|
return Math.ceil(value);
|
||||||
|
default:
|
||||||
|
return Math.round(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function logicalSize(output, compositor) {
|
function logicalSize(output, compositor) {
|
||||||
if (!output)
|
if (!output)
|
||||||
return {
|
return {
|
||||||
@@ -841,19 +851,10 @@ function logicalSize(output, compositor) {
|
|||||||
};
|
};
|
||||||
const phys = physicalSize(output);
|
const phys = physicalSize(output);
|
||||||
const scale = output.logical?.scale || 1.0;
|
const scale = output.logical?.scale || 1.0;
|
||||||
switch (compositor) {
|
return {
|
||||||
case "niri":
|
"w": roundLogical(phys.w / scale, compositor),
|
||||||
// niri floors logical sizes, rounding up by one leaves a dead seam at snapped edges (#2526)
|
"h": roundLogical(phys.h / scale, compositor)
|
||||||
return {
|
};
|
||||||
"w": Math.floor(phys.w / scale),
|
|
||||||
"h": Math.floor(phys.h / scale)
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
"w": Math.round(phys.w / scale),
|
|
||||||
"h": Math.round(phys.h / scale)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultBounds() {
|
function defaultBounds() {
|
||||||
@@ -991,8 +992,8 @@ function recalculateAdjacentPositions(outputs, pendingChanges, changedOutput, ne
|
|||||||
if (!output?.logical)
|
if (!output?.logical)
|
||||||
return [];
|
return [];
|
||||||
const oldPhys = physicalSize(output);
|
const oldPhys = physicalSize(output);
|
||||||
const oldLogicalW = Math.round(oldPhys.w / (output.logical.scale || 1.0));
|
const oldLogicalW = roundLogical(oldPhys.w / (output.logical.scale || 1.0), compositor);
|
||||||
const newLogicalW = Math.round(oldPhys.w / newScale);
|
const newLogicalW = roundLogical(oldPhys.w / newScale, compositor);
|
||||||
const changedX = pendingChanges[changedOutput]?.position?.x ?? output.logical.x;
|
const changedX = pendingChanges[changedOutput]?.position?.x ?? output.logical.x;
|
||||||
const changedY = pendingChanges[changedOutput]?.position?.y ?? output.logical.y;
|
const changedY = pendingChanges[changedOutput]?.position?.y ?? output.logical.y;
|
||||||
const moves = [];
|
const moves = [];
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int sessionConfigVersion: 7
|
readonly property int sessionConfigVersion: 7
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
signal loaded
|
signal loaded
|
||||||
signal brightnessDisplayHintChanged(string deviceName)
|
signal brightnessDisplayHintChanged(string deviceName)
|
||||||
signal loadErrorOccurred(string file, string message)
|
signal loadErrorOccurred(string file, string message)
|
||||||
@@ -326,11 +324,6 @@ Singleton {
|
|||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
_pendingMigration = null;
|
_pendingMigration = null;
|
||||||
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txt = settingsFile.text();
|
const txt = settingsFile.text();
|
||||||
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
let obj = (txt && txt.trim()) ? JSON.parse(txt) : null;
|
||||||
@@ -369,7 +362,7 @@ Singleton {
|
|||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
|
|
||||||
if (!isGreeterMode && typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
Theme.generateSystemThemesFromCurrentTheme();
|
Theme.generateSystemThemesFromCurrentTheme();
|
||||||
|
|
||||||
loaded();
|
loaded();
|
||||||
@@ -452,7 +445,7 @@ Singleton {
|
|||||||
_loadedSessionSnapshot = getCurrentSessionJson();
|
_loadedSessionSnapshot = getCurrentSessionJson();
|
||||||
_hasLoaded = true;
|
_hasLoaded = true;
|
||||||
|
|
||||||
if (!isGreeterMode && typeof Theme !== "undefined")
|
if (typeof Theme !== "undefined")
|
||||||
Theme.generateSystemThemesFromCurrentTheme();
|
Theme.generateSystemThemesFromCurrentTheme();
|
||||||
|
|
||||||
loaded();
|
loaded();
|
||||||
@@ -485,7 +478,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (isGreeterMode || _parseError || !_hasLoaded)
|
if (_parseError || !_hasLoaded)
|
||||||
return;
|
return;
|
||||||
settingsFile.setText(getCurrentSessionJson());
|
settingsFile.setText(getCurrentSessionJson());
|
||||||
if (_isReadOnly)
|
if (_isReadOnly)
|
||||||
@@ -859,8 +852,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _screenShareShouldHold() {
|
function _screenShareShouldHold() {
|
||||||
if (isGreeterMode)
|
|
||||||
return false;
|
|
||||||
if (typeof SettingsData === "undefined" || !SettingsData.notificationDndWhileScreenSharing)
|
if (typeof SettingsData === "undefined" || !SettingsData.notificationDndWhileScreenSharing)
|
||||||
return false;
|
return false;
|
||||||
if (typeof PrivacyService === "undefined" || !PrivacyService.screensharingActive)
|
if (typeof PrivacyService === "undefined" || !PrivacyService.screensharingActive)
|
||||||
@@ -909,9 +900,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncScreenShareDnd() {
|
function syncScreenShareDnd() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_screenShareShouldHold()) {
|
if (!_screenShareShouldHold()) {
|
||||||
if (!doNotDisturbHeldByScreenShare && !screenShareDndDismissed)
|
if (!doNotDisturbHeldByScreenShare && !screenShareDndDismissed)
|
||||||
return;
|
return;
|
||||||
@@ -1883,14 +1871,12 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: settingsFile
|
id: settingsFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/dms/session.json"
|
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/dms/session.json"
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
_hasUnsavedChanges = false;
|
_hasUnsavedChanges = false;
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
@@ -1900,39 +1886,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string greeterSessionBaseDir: root._greeterCacheDir
|
|
||||||
|
|
||||||
function setGreeterSessionBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (greeterSessionBaseDir === next)
|
|
||||||
return;
|
|
||||||
greeterSessionBaseDir = next;
|
|
||||||
if (isGreeterMode)
|
|
||||||
greeterSessionFile.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGreeterSessionBaseDir() {
|
|
||||||
setGreeterSessionBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: greeterSessionFile
|
|
||||||
|
|
||||||
path: isGreeterMode && root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : ""
|
|
||||||
preload: isGreeterMode
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: true
|
|
||||||
onLoaded: {
|
|
||||||
if (isGreeterMode) {
|
|
||||||
parseSettings(greeterSessionFile.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: sessionWritableCheckProcess
|
id: sessionWritableCheckProcess
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ Singleton {
|
|||||||
|
|
||||||
readonly property int settingsConfigVersion: 42
|
readonly property int settingsConfigVersion: 42
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
|
||||||
|
|
||||||
enum Position {
|
enum Position {
|
||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
@@ -1435,8 +1433,6 @@ Singleton {
|
|||||||
signal notificationPopupsInvalidated
|
signal notificationPopupsInvalidated
|
||||||
|
|
||||||
function refreshAuthAvailability() {
|
function refreshAuthAvailability() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Processes.detectAuthCapabilities();
|
Processes.detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1506,8 +1502,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIconThemeDrift() {
|
function checkIconThemeDrift() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
if (resolveIconTheme() === "System Default")
|
if (resolveIconTheme() === "System Default")
|
||||||
return;
|
return;
|
||||||
if (!SessionData.lastAppliedIconTheme)
|
if (!SessionData.lastAppliedIconTheme)
|
||||||
@@ -1664,8 +1658,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
function scheduleAuthApply() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
Processes.scheduleAuthApply();
|
Processes.scheduleAuthApply();
|
||||||
@@ -1673,8 +1665,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduleGreeterAutoLoginSync() {
|
function scheduleGreeterAutoLoginSync() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
Processes.settingsRoot = root;
|
Processes.settingsRoot = root;
|
||||||
Processes.scheduleGreeterAutoLoginSync();
|
Processes.scheduleGreeterAutoLoginSync();
|
||||||
@@ -1689,8 +1679,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markGreeterSyncPending(who, key, oldValue) {
|
function markGreeterSyncPending(who, key, oldValue) {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
if (!(key in SessionData.greeterSyncBaseline)) {
|
if (!(key in SessionData.greeterSyncBaseline)) {
|
||||||
var baseline = Object.assign({}, SessionData.greeterSyncBaseline);
|
var baseline = Object.assign({}, SessionData.greeterSyncBaseline);
|
||||||
baseline[key] = oldValue;
|
baseline[key] = oldValue;
|
||||||
@@ -1840,7 +1828,7 @@ Singleton {
|
|||||||
let loadedSettings = JSON.stringify(obj);
|
let loadedSettings = JSON.stringify(obj);
|
||||||
|
|
||||||
const oldVersion = obj?.configVersion ?? 0;
|
const oldVersion = obj?.configVersion ?? 0;
|
||||||
if (oldVersion > settingsConfigVersion && !isGreeterMode) {
|
if (oldVersion > settingsConfigVersion) {
|
||||||
newerSettingsBackup.path = `${_configDir}/dms/settings.v${oldVersion}.json`;
|
newerSettingsBackup.path = `${_configDir}/dms/settings.v${oldVersion}.json`;
|
||||||
newerSettingsBackup.setText(JSON.stringify(obj, null, 2));
|
newerSettingsBackup.setText(JSON.stringify(obj, null, 2));
|
||||||
}
|
}
|
||||||
@@ -2063,16 +2051,14 @@ Singleton {
|
|||||||
return splitSettings;
|
return splitSettings;
|
||||||
}
|
}
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
if (isGreeterMode || _loading || !_hasLoaded)
|
if (_loading || !_hasLoaded)
|
||||||
return;
|
return;
|
||||||
unsavedUserChanges = true;
|
unsavedUserChanges = true;
|
||||||
settingsSaveDebounce.restart();
|
settingsSaveDebounce.restart();
|
||||||
}
|
}
|
||||||
function _saveSettings(userChanges) {
|
function _saveSettings(userChanges) {
|
||||||
let reason = null;
|
let reason = null;
|
||||||
if (isGreeterMode) {
|
if (_loading) {
|
||||||
reason = "running in greeter mode.";
|
|
||||||
} else if (_loading) {
|
|
||||||
reason = "some files are being loaded";
|
reason = "some files are being loaded";
|
||||||
} else if (_parseError) {
|
} else if (_parseError) {
|
||||||
reason = "failed to parse settings.";
|
reason = "failed to parse settings.";
|
||||||
@@ -2092,7 +2078,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function savePluginSettings() {
|
function savePluginSettings() {
|
||||||
if (isGreeterMode || _pluginSettingsLoading || _pluginParseError)
|
if (_pluginSettingsLoading || _pluginParseError)
|
||||||
return;
|
return;
|
||||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
|
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
|
||||||
}
|
}
|
||||||
@@ -3725,11 +3711,11 @@ Singleton {
|
|||||||
property FileView fileView: FileView {
|
property FileView fileView: FileView {
|
||||||
id: settingsFileView
|
id: settingsFileView
|
||||||
|
|
||||||
path: isGreeterMode ? "" : filePath
|
path: filePath
|
||||||
blockLoading: false
|
blockLoading: false
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onFileChanged: {
|
onFileChanged: {
|
||||||
if (selfWrite) {
|
if (selfWrite) {
|
||||||
selfWrite = false;
|
selfWrite = false;
|
||||||
@@ -3740,9 +3726,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
_loading = true;
|
_loading = true;
|
||||||
const hadParseFailed = hasParseFailed;
|
const hadParseFailed = hasParseFailed;
|
||||||
@@ -3776,9 +3759,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (isGreeterMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
_loading = _anySettingsFile(file => file.isLoading);
|
_loading = _anySettingsFile(file => file.isLoading);
|
||||||
if (error === FileViewError.FileNotFound) {
|
if (error === FileViewError.FileNotFound) {
|
||||||
@@ -3922,7 +3902,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool _settingsFolderExists: false
|
property bool _settingsFolderExists: false
|
||||||
property bool _settingsFolderChecked: isGreeterMode
|
property bool _settingsFolderChecked: false
|
||||||
function _syncSettingsFilesModels() {
|
function _syncSettingsFilesModels() {
|
||||||
if (!_settingsFolderChecked) {
|
if (!_settingsFolderChecked) {
|
||||||
settingsFilesModelSyncDebounce.restart();
|
settingsFilesModelSyncDebounce.restart();
|
||||||
@@ -3963,7 +3943,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
id: settingsFolderLoader
|
id: settingsFolderLoader
|
||||||
active: !isGreeterMode
|
|
||||||
function unloadModel() {
|
function unloadModel() {
|
||||||
active = false;
|
active = false;
|
||||||
}
|
}
|
||||||
@@ -4012,59 +3991,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string greeterSettingsBaseDir: root._greeterCacheDir
|
|
||||||
|
|
||||||
function setGreeterSettingsBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (greeterSettingsBaseDir === next)
|
|
||||||
return;
|
|
||||||
greeterSettingsBaseDir = next;
|
|
||||||
if (isGreeterMode)
|
|
||||||
greeterSettingsFile.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGreeterSettingsBaseDir() {
|
|
||||||
setGreeterSettingsBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadGreeterSettings(txt) {
|
|
||||||
_loading = true;
|
|
||||||
_hasLoaded = false;
|
|
||||||
try {
|
|
||||||
Store.parse(root, (txt && txt.trim()) ? JSON.parse(txt) : {});
|
|
||||||
_parseError = false;
|
|
||||||
} catch (e) {
|
|
||||||
_parseError = true;
|
|
||||||
log.error("Failed to parse greeter settings.json:", e.message);
|
|
||||||
Store.parse(root, {});
|
|
||||||
} finally {
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
_hasLoaded = true;
|
|
||||||
applyStoredTheme();
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: greeterSettingsFile
|
|
||||||
|
|
||||||
path: isGreeterMode && root.greeterSettingsBaseDir ? (root.greeterSettingsBaseDir + "/settings.json") : ""
|
|
||||||
preload: isGreeterMode
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
if (isGreeterMode)
|
|
||||||
loadGreeterSettings(greeterSettingsFile.text());
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
if (isGreeterMode)
|
|
||||||
loadGreeterSettings("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our first save drops the keys a newer shell wrote, so its settings are kept for going back.
|
// Our first save drops the keys a newer shell wrote, so its settings are kept for going back.
|
||||||
FileView {
|
FileView {
|
||||||
id: newerSettingsBackup
|
id: newerSettingsBackup
|
||||||
@@ -4078,21 +4004,17 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: pluginSettingsFile
|
id: pluginSettingsFile
|
||||||
|
|
||||||
path: isGreeterMode ? "" : pluginSettingsPath
|
path: pluginSettingsPath
|
||||||
blockLoading: true
|
blockLoading: true
|
||||||
blockWrites: true
|
blockWrites: true
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
printErrors: false
|
printErrors: false
|
||||||
watchChanges: !isGreeterMode
|
watchChanges: true
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
pluginSettingsFileExists = true;
|
pluginSettingsFileExists = true;
|
||||||
parsePluginSettings(pluginSettingsFile.text());
|
parsePluginSettings(pluginSettingsFile.text());
|
||||||
}
|
}
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
const msg = String(error || "");
|
const msg = String(error || "");
|
||||||
const missing = _isMissingPluginSettingsError(error);
|
const missing = _isMissingPluginSettingsError(error);
|
||||||
pluginSettingsFileExists = !missing;
|
pluginSettingsFileExists = !missing;
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ Singleton {
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "dock_general",
|
"id": "dock_general",
|
||||||
|
"hidden": true,
|
||||||
"text": I18n.tr("General"),
|
"text": I18n.tr("General"),
|
||||||
"icon": "tune",
|
"icon": "tune",
|
||||||
"tabIndex": 5,
|
"tabIndex": 5,
|
||||||
@@ -167,6 +168,7 @@ Singleton {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dock_widgets",
|
"id": "dock_widgets",
|
||||||
|
"hidden": true,
|
||||||
"text": I18n.tr("Apps & widgets"),
|
"text": I18n.tr("Apps & widgets"),
|
||||||
"icon": "widgets",
|
"icon": "widgets",
|
||||||
"tabIndex": 57,
|
"tabIndex": 57,
|
||||||
@@ -174,6 +176,7 @@ Singleton {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dock_appearance",
|
"id": "dock_appearance",
|
||||||
|
"hidden": true,
|
||||||
"text": I18n.tr("Appearance"),
|
"text": I18n.tr("Appearance"),
|
||||||
"icon": "palette",
|
"icon": "palette",
|
||||||
"tabIndex": 58,
|
"tabIndex": 58,
|
||||||
|
|||||||
+43
-51
@@ -13,7 +13,6 @@ import Quickshell.Io
|
|||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.DCommon.Common as DCommon
|
import qs.DCommon.Common as DCommon
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Modules.Greetd
|
|
||||||
import "StockThemes.js" as StockThemes
|
import "StockThemes.js" as StockThemes
|
||||||
import "GSettings.js" as GSettings
|
import "GSettings.js" as GSettings
|
||||||
|
|
||||||
@@ -1191,13 +1190,7 @@ Singleton {
|
|||||||
readonly property real groupedListInnerRadius: cornerRadiusXS
|
readonly property real groupedListInnerRadius: cornerRadiusXS
|
||||||
readonly property real groupedListOuterRadius: cornerRadiusL
|
readonly property real groupedListOuterRadius: cornerRadiusL
|
||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DCommon.Fonts.sans
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return DCommon.Fonts.sans;
|
|
||||||
if (SettingsData.isGreeterMode && SettingsData.lockScreenFontFamily !== "")
|
|
||||||
return resolvedFontFamily(SettingsData.lockScreenFontFamily);
|
|
||||||
return resolvedFontFamily(SettingsData.fontFamily);
|
|
||||||
}
|
|
||||||
|
|
||||||
property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DCommon.Fonts.mono
|
property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DCommon.Fonts.mono
|
||||||
property string displayFontFamily: typeof SettingsData !== "undefined" ? resolvedDisplayFontFamily(SettingsData.displayFontFamily) : DCommon.Fonts.display
|
property string displayFontFamily: typeof SettingsData !== "undefined" ? resolvedDisplayFontFamily(SettingsData.displayFontFamily) : DCommon.Fonts.display
|
||||||
@@ -1422,15 +1415,12 @@ Singleton {
|
|||||||
currentThemeCategory = "generic";
|
currentThemeCategory = "generic";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (savePrefs && typeof SettingsData !== "undefined") {
|
||||||
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) {
|
|
||||||
SettingsData.set("currentThemeCategory", currentThemeCategory);
|
SettingsData.set("currentThemeCategory", currentThemeCategory);
|
||||||
SettingsData.set("currentThemeName", currentTheme);
|
SettingsData.set("currentThemeName", currentTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGreeterMode) {
|
generateSystemThemesFromCurrentTheme();
|
||||||
generateSystemThemesFromCurrentTheme();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
function setLightMode(light, savePrefs = true, enableTransition = false) {
|
||||||
@@ -1447,17 +1437,14 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (savePrefs && typeof SessionData !== "undefined") {
|
||||||
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
|
|
||||||
SessionData.setLightMode(light);
|
SessionData.setLightMode(light);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGreeterMode) {
|
if (typeof SettingsData !== "undefined") {
|
||||||
if (typeof SettingsData !== "undefined") {
|
SettingsData.updateCosmicThemeMode(light);
|
||||||
SettingsData.updateCosmicThemeMode(light);
|
|
||||||
}
|
|
||||||
generateSystemThemesFromCurrentTheme();
|
|
||||||
}
|
}
|
||||||
|
generateSystemThemesFromCurrentTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLightMode(savePrefs = true) {
|
function toggleLightMode(savePrefs = true) {
|
||||||
@@ -1876,9 +1863,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runKey = Qt.md5(JSON.stringify(args));
|
const runKey = Qt.md5(JSON.stringify(args));
|
||||||
const skipUnchanged = !_matugenRunKey && !colorsFileLoadFailed && typeof SettingsData !== "undefined" && !SettingsData.generateThemeAtStartup;
|
const startupRun = !_matugenRunKey && typeof SettingsData !== "undefined" && SettingsData.generateThemeAtStartup;
|
||||||
if (skipUnchanged && runKey === CacheData.matugenAppliedKey) {
|
if (!startupRun && !colorsFileLoadFailed && runKey === CacheData.matugenAppliedKey) {
|
||||||
log.info("Theme inputs unchanged since the last run, skipping startup generation");
|
log.info("Theme inputs unchanged since the last run, skipping generation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1938,8 +1925,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateSystemThemesFromCurrentTheme() {
|
function generateSystemThemesFromCurrentTheme() {
|
||||||
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode);
|
if (!matugenAvailable)
|
||||||
if (!matugenAvailable || isGreeterMode)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_pendingGenerateParams = true;
|
_pendingGenerateParams = true;
|
||||||
@@ -2395,32 +2381,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string greeterColorsBaseDir: root._greeterCacheDir
|
|
||||||
|
|
||||||
function setGreeterColorsBaseDir(dir) {
|
|
||||||
const next = dir || root._greeterCacheDir;
|
|
||||||
if (greeterColorsBaseDir === next)
|
|
||||||
return;
|
|
||||||
greeterColorsBaseDir = next;
|
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode)
|
|
||||||
dynamicColorsFileView.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGreeterColorsBaseDir() {
|
|
||||||
setGreeterColorsBaseDir(root._greeterCacheDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: stateDir + "/dms-colors.json"
|
||||||
if (SessionData.isGreeterMode)
|
|
||||||
return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : "";
|
|
||||||
return stateDir + "/dms-colors.json";
|
|
||||||
}
|
|
||||||
blockLoading: false
|
blockLoading: false
|
||||||
watchChanges: !SessionData.isGreeterMode
|
watchChanges: true
|
||||||
|
|
||||||
function parseAndLoadColors() {
|
function parseAndLoadColors() {
|
||||||
try {
|
try {
|
||||||
@@ -2462,9 +2427,6 @@ Singleton {
|
|||||||
if (currentTheme !== dynamic)
|
if (currentTheme !== dynamic)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (SessionData.isGreeterMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (workerRunning) {
|
if (workerRunning) {
|
||||||
colorsReloadRetry.restart();
|
colorsReloadRetry.restart();
|
||||||
return;
|
return;
|
||||||
@@ -2496,6 +2458,36 @@ Singleton {
|
|||||||
onTriggered: dynamicColorsFileView.reload()
|
onTriggered: dynamicColorsFileView.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property bool publishTypography: typeof SettingsData !== "undefined" && SettingsData._hasLoaded
|
||||||
|
readonly property string typographyJson: JSON.stringify({
|
||||||
|
"fontFamily": fontFamily,
|
||||||
|
"monoFontFamily": monoFontFamily,
|
||||||
|
"displayFontFamily": displayFontFamily,
|
||||||
|
"fontWeight": fontWeight,
|
||||||
|
"fontScale": fontScale
|
||||||
|
}, null, 2)
|
||||||
|
|
||||||
|
onPublishTypographyChanged: typographyPublishDebounce.restart()
|
||||||
|
onTypographyJsonChanged: typographyPublishDebounce.restart()
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: typographyFileView
|
||||||
|
path: root.publishTypography ? root.stateDir + "/dms-typography.json" : ""
|
||||||
|
blockLoading: false
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: typographyPublishDebounce
|
||||||
|
interval: 250
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (!root.publishTypography)
|
||||||
|
return;
|
||||||
|
typographyFileView.setText(root.typographyJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "theme"
|
target: "theme"
|
||||||
|
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ function mangoActiveWorkspace(raw, screenName) {
|
|||||||
|
|
||||||
function mangoWindowsOnWorkspace(windows, workspace) {
|
function mangoWindowsOnWorkspace(windows, workspace) {
|
||||||
// mangoTags are 1-based; workspace.id is 0-based.
|
// mangoTags are 1-based; workspace.id is 0-based.
|
||||||
return windows.filter(win => win && (win.mangoTags || []).includes(workspace.id + 1));
|
return windows.filter(win => win && win.mangoMonitor === workspace.output && (win.mangoTags || []).includes(workspace.id + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function i3Key(ws) {
|
function i3Key(ws) {
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ Singleton {
|
|||||||
property var settingsRoot: null
|
property var settingsRoot: null
|
||||||
|
|
||||||
onSettingsRootChanged: {
|
onSettingsRootChanged: {
|
||||||
if (settingsRoot && !settingsRoot.isGreeterMode)
|
if (settingsRoot)
|
||||||
consumeGreeterAutoLoginPendingSync();
|
consumeGreeterAutoLoginPendingSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
|
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
|
||||||
|
|
||||||
function consumeGreeterAutoLoginPendingSync() {
|
function consumeGreeterAutoLoginPendingSync() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
greeterAutoLoginPendingCheckProcess.running = true;
|
greeterAutoLoginPendingCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ Singleton {
|
|||||||
property string authApplyTerminalFallbackStderr: ""
|
property string authApplyTerminalFallbackStderr: ""
|
||||||
|
|
||||||
function scheduleAuthApply() {
|
function scheduleAuthApply() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
authApplyQueued = true;
|
authApplyQueued = true;
|
||||||
@@ -305,7 +305,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginAuthApply() {
|
function beginAuthApply() {
|
||||||
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
if (!authApplyQueued || authApplyRunning || !settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
authApplyQueued = false;
|
authApplyQueued = false;
|
||||||
@@ -344,7 +344,7 @@ Singleton {
|
|||||||
property string greeterAutoLoginSyncStderr: ""
|
property string greeterAutoLoginSyncStderr: ""
|
||||||
|
|
||||||
function scheduleGreeterAutoLoginSync() {
|
function scheduleGreeterAutoLoginSync() {
|
||||||
if (!settingsRoot || settingsRoot.isGreeterMode)
|
if (!settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
greeterAutoLoginSyncQueued = true;
|
greeterAutoLoginSyncQueued = true;
|
||||||
@@ -357,7 +357,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function beginGreeterAutoLoginSync() {
|
function beginGreeterAutoLoginSync() {
|
||||||
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode)
|
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
greeterAutoLoginSyncQueued = false;
|
greeterAutoLoginSyncQueued = false;
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Modules.Greetd
|
|
||||||
|
|
||||||
Scope {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
GreeterSurface {}
|
|
||||||
}
|
|
||||||
@@ -1102,6 +1102,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function set(key: string, value: string): string {
|
function set(key: string, value: string): string {
|
||||||
|
if (AdminPolicy.disableSettingsIpcSet)
|
||||||
|
return "SETTINGS_SET_DISABLED";
|
||||||
if (!(key in SettingsData)) {
|
if (!(key in SettingsData)) {
|
||||||
log.warn("Cannot set property, not found:", key);
|
log.warn("Cannot set property, not found:", key);
|
||||||
return "SETTINGS_INVALID_KEY";
|
return "SETTINGS_INVALID_KEY";
|
||||||
|
|||||||
@@ -744,8 +744,12 @@ Item {
|
|||||||
readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + surfaceSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel && spansEdge ? _wingR : 0), _dpr) + _shadowBuffer
|
readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + surfaceSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel && spansEdge ? _wingR : 0), _dpr) + _shadowBuffer
|
||||||
readonly property bool islandSheetOut: islandHost?.sheetOut ?? false
|
readonly property bool islandSheetOut: islandHost?.sheetOut ?? false
|
||||||
readonly property real islandSatelliteShadowBuffer: isIsland && islandSatellitesEnabled && !islandSatelliteBackground ? _shadowBuffer : 0
|
readonly property real islandSatelliteShadowBuffer: isIsland && islandSatellitesEnabled && !islandSatelliteBackground ? _shadowBuffer : 0
|
||||||
readonly property real hostThickness: isIsland ? Math.max(islandHost?.hostThickness ?? 0, islandStripThickness + islandSatelliteShadowBuffer) : Math.max(barSurfaceThickness, islandSheetOut ? islandHost.hostThickness : 0)
|
// A grown layer surface jumps to its new origin before the client draws at the new size, so a far-edge host never grows for the sheet.
|
||||||
readonly property real hideSlideThickness: isIsland ? hostThickness : barSurfaceThickness
|
readonly property bool islandReservesSheet: !!islandHost && (axis.edge === "bottom" || axis.edge === "right")
|
||||||
|
readonly property real hostThickness: isIsland ? Math.max(islandHost?.hostThickness ?? 0, islandStripThickness + islandSatelliteShadowBuffer) : Math.max(barSurfaceThickness, islandSheetOut || islandReservesSheet ? islandHost.hostThickness : 0)
|
||||||
|
// The host reserves the expanded island's height, so sliding by it would move a compact island off-screen within a frame.
|
||||||
|
readonly property real islandCompactThickness: Math.max(islandStripThickness + islandSatelliteShadowBuffer, (islandHost?.outerGap ?? 0) + (islandHost?.compactThickness ?? 0) + (islandHost?.shadowPadding ?? 0))
|
||||||
|
readonly property real hideSlideThickness: isIsland ? islandCompactThickness : barSurfaceThickness
|
||||||
readonly property real surfaceImplicitHeight: !isVertical ? hostThickness : 0
|
readonly property real surfaceImplicitHeight: !isVertical ? hostThickness : 0
|
||||||
readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0
|
readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0
|
||||||
|
|
||||||
|
|||||||
@@ -30,35 +30,29 @@ BasePill {
|
|||||||
readonly property bool showPower: BatteryService.isCharging ? showPowerCharging : showPowerDischarging
|
readonly property bool showPower: BatteryService.isCharging ? showPowerCharging : showPowerDischarging
|
||||||
readonly property bool critical: SettingsData.batteryCriticalAnimation && BatteryService.isCriticalBattery && !BatteryService.isCharging
|
readonly property bool critical: SettingsData.batteryCriticalAnimation && BatteryService.isCriticalBattery && !BatteryService.isCharging
|
||||||
property real criticalPulse: 0
|
property real criticalPulse: 0
|
||||||
readonly property color criticalForeground: battery.mixColor(Theme.error, Theme.surface, criticalPulse)
|
readonly property color criticalForeground: noBackground ? battery.mixColor(Theme.error, defaultContentColor, criticalPulse) : battery.mixColor(Theme.error, Theme.surface, criticalPulse)
|
||||||
|
|
||||||
fillColor: critical ? battery.mixColor(defaultFillColor, Theme.error, criticalPulse) : defaultFillColor
|
fillColor: critical && !noBackground ? battery.mixColor(defaultFillColor, Theme.error, criticalPulse) : defaultFillColor
|
||||||
contentColor: critical ? battery.mixColor(defaultContentColor, Theme.surface, criticalPulse) : defaultContentColor
|
contentColor: critical ? (noBackground ? criticalForeground : battery.mixColor(defaultContentColor, Theme.surface, criticalPulse)) : defaultContentColor
|
||||||
|
|
||||||
function mixColor(from, to, progress) {
|
function mixColor(from, to, progress) {
|
||||||
return Qt.rgba(from.r + (to.r - from.r) * progress, from.g + (to.g - from.g) * progress, from.b + (to.b - from.b) * progress, from.a + (to.a - from.a) * progress);
|
return Qt.rgba(from.r + (to.r - from.r) * progress, from.g + (to.g - from.g) * progress, from.b + (to.b - from.b) * progress, from.a + (to.a - from.a) * progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation on criticalPulse {
|
property int criticalTick: 0
|
||||||
|
|
||||||
|
// Stepped on purpose: an Animation repaints the bar at the full refresh rate.
|
||||||
|
Timer {
|
||||||
|
interval: 50
|
||||||
|
repeat: true
|
||||||
running: battery.critical && battery.surfaceLive && !SettingsData.reduceMotion
|
running: battery.critical && battery.surfaceLive && !SettingsData.reduceMotion
|
||||||
loops: Animation.Infinite
|
|
||||||
onRunningChanged: {
|
onRunningChanged: {
|
||||||
if (!running)
|
battery.criticalTick = 0;
|
||||||
battery.criticalPulse = 0;
|
battery.criticalPulse = 0;
|
||||||
}
|
}
|
||||||
|
onTriggered: {
|
||||||
NumberAnimation {
|
battery.criticalTick = (battery.criticalTick + 1) % 40;
|
||||||
from: 0
|
battery.criticalPulse = 1 - Math.abs(20 - battery.criticalTick) / 20;
|
||||||
to: 1
|
|
||||||
duration: 1000
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: 1000
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
property bool isOccupied: root.useNativeWorkspaces && CompositorService.workspaceOccupied(record)
|
property bool isOccupied: root.useNativeWorkspaces && CompositorService.workspaceOccupied(record)
|
||||||
property bool isPlaceholder: !!(record && record.placeholder)
|
property bool isPlaceholder: !!(record && record.placeholder)
|
||||||
property bool isHovered: mouseArea.containsMouse
|
property bool isHovered: pillHover.hovered
|
||||||
|
|
||||||
property bool loadedIsUrgent: false
|
property bool loadedIsUrgent: false
|
||||||
property bool isUrgent: root.useNativeWorkspaces ? CompositorService.workspaceUrgent(record, loadedIsUrgent) : (modelData?.urgent ?? false)
|
property bool isUrgent: root.useNativeWorkspaces ? CompositorService.workspaceUrgent(record, loadedIsUrgent) : (modelData?.urgent ?? false)
|
||||||
@@ -615,6 +615,11 @@ BasePill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: pillHover
|
||||||
|
enabled: !isPlaceholder
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
@@ -680,6 +680,8 @@ DPopout {
|
|||||||
readonly property bool ready: targetHeight >= 0 && settledHeight === targetHeight
|
readonly property bool ready: targetHeight >= 0 && settledHeight === targetHeight
|
||||||
readonly property real availableHeight: root.screen ? root.screen.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight
|
readonly property real availableHeight: root.screen ? root.screen.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight
|
||||||
readonly property int rowBudget: DashMetrics.rowCapFor(availableHeight)
|
readonly property int rowBudget: DashMetrics.rowCapFor(availableHeight)
|
||||||
|
// Growing a panel not hung from a top bar moves the grid under a held card, which retargets the drop.
|
||||||
|
readonly property bool heightHeld: (currentItem?.cardInteracting ?? false) && root.effectiveBarPosition % 4 !== SettingsData.Top
|
||||||
|
|
||||||
x: (root.verticalNavigation && root.navigationEdge === SettingsData.Left ? root.navigationWidth : 0) - root.editGutter
|
x: (root.verticalNavigation && root.navigationEdge === SettingsData.Left ? root.navigationWidth : 0) - root.editGutter
|
||||||
y: (!root.verticalNavigation && root.navigationEdge !== SettingsData.Bottom ? mainContainer.horizontalChromeHeight : 0) - root.editGutter
|
y: (!root.verticalNavigation && root.navigationEdge !== SettingsData.Bottom ? mainContainer.horizontalChromeHeight : 0) - root.editGutter
|
||||||
@@ -691,12 +693,13 @@ DPopout {
|
|||||||
clip: contentHeight > height
|
clip: contentHeight > height
|
||||||
|
|
||||||
function updateContentHeight() {
|
function updateContentHeight() {
|
||||||
if (targetHeight < 0)
|
if (targetHeight < 0 || heightHeld)
|
||||||
return;
|
return;
|
||||||
settledHeight = targetHeight;
|
settledHeight = targetHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
onTargetHeightChanged: Qt.callLater(updateContentHeight)
|
onTargetHeightChanged: Qt.callLater(updateContentHeight)
|
||||||
|
onHeightHeldChanged: Qt.callLater(updateContentHeight)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: emptyContent
|
id: emptyContent
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ Singleton {
|
|||||||
readonly property int mediaLyricsRequestTimeout: 16000
|
readonly property int mediaLyricsRequestTimeout: 16000
|
||||||
readonly property int mediaLyricsLoadingDelay: 300
|
readonly property int mediaLyricsLoadingDelay: 300
|
||||||
readonly property real mediaLyricsPositionTolerance: 0.05
|
readonly property real mediaLyricsPositionTolerance: 0.05
|
||||||
|
readonly property real mediaLyricsWholeSecondSlack: 0.005
|
||||||
readonly property int mediaLyricsPositionProbeInterval: 5000
|
readonly property int mediaLyricsPositionProbeInterval: 5000
|
||||||
readonly property real mediaLyricsPositionProbeDrift: 1.5
|
readonly property real mediaLyricsPositionProbeDrift: 1.5
|
||||||
readonly property real mediaLyricsPositionProbeThreshold: 1
|
readonly property real mediaLyricsPositionProbeThreshold: 1
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Item {
|
|||||||
readonly property Item focusTarget: grid
|
readonly property Item focusTarget: grid
|
||||||
readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown
|
readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown
|
||||||
readonly property bool cardResizing: grid.sizePreview !== null
|
readonly property bool cardResizing: grid.sizePreview !== null
|
||||||
|
readonly property bool cardInteracting: grid.interacting
|
||||||
|
|
||||||
implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight)
|
implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight)
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Item {
|
|||||||
interval: DashMetrics.mediaPositionPollInterval
|
interval: DashMetrics.mediaPositionPollInterval
|
||||||
running: root.live && root.activePlayer?.playbackState === MprisPlaybackState.Playing && !root.isSeeking
|
running: root.live && root.activePlayer?.playbackState === MprisPlaybackState.Playing && !root.isSeeking
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: root.activePlayer?.positionChanged()
|
onTriggered: MprisController.refreshPosition(root.activePlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAudioRef(wanted) {
|
function syncAudioRef(wanted) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Card {
|
|||||||
interval: DashMetrics.mediaPositionPollInterval
|
interval: DashMetrics.mediaPositionPollInterval
|
||||||
running: root.live && root.visible && root.interactive && root.showSeekbar && root.playing && !root.isSeeking
|
running: root.live && root.visible && root.interactive && root.showSeekbar && root.playing && !root.isSeeking
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: root.activePlayer?.positionSupported && root.activePlayer.positionChanged()
|
onTriggered: MprisController.refreshPosition(root.activePlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ FocusScope {
|
|||||||
readonly property var addable: grid.addableEntries
|
readonly property var addable: grid.addableEntries
|
||||||
readonly property int usedColumns: grid.usedColumns
|
readonly property int usedColumns: grid.usedColumns
|
||||||
readonly property bool cardResizing: grid.sizePreview !== null
|
readonly property bool cardResizing: grid.sizePreview !== null
|
||||||
|
readonly property bool cardInteracting: grid.interacting
|
||||||
readonly property bool blocksTabNavigation: grid.blocksTabNavigation || detailSheet.shown || editorSheet.shown || optionsSheet.shown || addMenu.open
|
readonly property bool blocksTabNavigation: grid.blocksTabNavigation || detailSheet.shown || editorSheet.shown || optionsSheet.shown || addMenu.open
|
||||||
|
|
||||||
implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber)
|
implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Item {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
readonly property bool blocksTabNavigation: widgets.blocksTabNavigation
|
readonly property bool blocksTabNavigation: widgets.blocksTabNavigation
|
||||||
|
readonly property bool cardInteracting: widgets.cardInteracting
|
||||||
readonly property var forecastOptions: [DashRegistry.choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [
|
readonly property var forecastOptions: [DashRegistry.choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [
|
||||||
{
|
{
|
||||||
"value": "chart",
|
"value": "chart",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
.pragma library
|
|
||||||
|
|
||||||
function readBoolOverride(envReader, names, fallbackValue) {
|
|
||||||
for (let i = 0; i < names.length; i++) {
|
|
||||||
const name = names[i];
|
|
||||||
const raw = envReader(name);
|
|
||||||
if (raw === undefined || raw === null || raw === "")
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const normalized = String(raw).trim().toLowerCase();
|
|
||||||
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
|
||||||
return true;
|
|
||||||
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackValue;
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import "GreetdEnv.js" as GreetdEnv
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
readonly property var log: Log.scoped("GreetdMemory")
|
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
|
||||||
readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
|
|
||||||
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
|
||||||
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
|
||||||
|
|
||||||
property string lastSessionId: ""
|
|
||||||
property string lastSessionDesktopId: ""
|
|
||||||
property string lastSessionExec: ""
|
|
||||||
property string lastSuccessfulUser: ""
|
|
||||||
property bool memoryReady: false
|
|
||||||
property bool isLightMode: false
|
|
||||||
property bool nightModeEnabled: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
loadMemory();
|
|
||||||
loadSessionConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadMemory() {
|
|
||||||
parseMemory(memoryFileView.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSessionConfig() {
|
|
||||||
parseSessionConfig(sessionConfigFileView.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSessionConfig(content) {
|
|
||||||
try {
|
|
||||||
if (content && content.trim()) {
|
|
||||||
const config = JSON.parse(content);
|
|
||||||
isLightMode = config.isLightMode !== undefined ? config.isLightMode : false;
|
|
||||||
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Failed to parse greeter session config:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMemory(content) {
|
|
||||||
try {
|
|
||||||
if (!content || !content.trim())
|
|
||||||
return;
|
|
||||||
const memory = JSON.parse(content);
|
|
||||||
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
|
||||||
lastSessionDesktopId = rememberLastSession ? (memory.lastSessionDesktopId || "") : "";
|
|
||||||
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
|
|
||||||
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
|
||||||
if (!rememberLastSession || !rememberLastUser)
|
|
||||||
saveMemory();
|
|
||||||
} catch (e) {
|
|
||||||
log.warn("Failed to parse greetd memory:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveMemory() {
|
|
||||||
let memory = {};
|
|
||||||
if (rememberLastSession && lastSessionId)
|
|
||||||
memory.lastSessionId = lastSessionId;
|
|
||||||
if (rememberLastSession && lastSessionDesktopId)
|
|
||||||
memory.lastSessionDesktopId = lastSessionDesktopId;
|
|
||||||
if (rememberLastUser && lastSuccessfulUser)
|
|
||||||
memory.lastSuccessfulUser = lastSuccessfulUser;
|
|
||||||
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSession(id, desktopId) {
|
|
||||||
if (!rememberLastSession) {
|
|
||||||
if (lastSessionId !== "" || lastSessionDesktopId !== "" || lastSessionExec !== "") {
|
|
||||||
lastSessionId = "";
|
|
||||||
lastSessionDesktopId = "";
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastSessionId = id || "";
|
|
||||||
lastSessionDesktopId = desktopId || "";
|
|
||||||
lastSessionExec = "";
|
|
||||||
if (!lastSessionId)
|
|
||||||
lastSessionDesktopId = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
|
||||||
setLastSession(id, lastSessionDesktopId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionDesktopId(id) {
|
|
||||||
setLastSession(lastSessionId, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSessionExec(exec) {
|
|
||||||
if (!rememberLastSession) {
|
|
||||||
if (lastSessionExec !== "") {
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (lastSessionExec !== "") {
|
|
||||||
lastSessionExec = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
|
||||||
if (!rememberLastUser) {
|
|
||||||
if (lastSuccessfulUser !== "") {
|
|
||||||
lastSuccessfulUser = "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastSuccessfulUser = username || "";
|
|
||||||
saveMemory();
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: memoryFileView
|
|
||||||
path: root.memoryFile
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: false
|
|
||||||
atomicWrites: true
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
parseMemory(memoryFileView.text());
|
|
||||||
root.memoryReady = true;
|
|
||||||
}
|
|
||||||
onLoadFailed: {
|
|
||||||
root.memoryReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: sessionConfigFileView
|
|
||||||
path: root.sessionConfigPath
|
|
||||||
blockLoading: false
|
|
||||||
blockWrites: true
|
|
||||||
atomicWrites: false
|
|
||||||
watchChanges: false
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: {
|
|
||||||
parseSessionConfig(sessionConfigFileView.text());
|
|
||||||
}
|
|
||||||
onLoadFailed: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import "GreetdEnv.js" as GreetdEnv
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var _envRememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined)
|
|
||||||
readonly property var _envRememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined)
|
|
||||||
|
|
||||||
readonly property bool settingsLoaded: SettingsData._hasLoaded
|
|
||||||
readonly property bool rememberLastSession: _envRememberLastSession !== undefined ? _envRememberLastSession : SettingsData.greeterRememberLastSession
|
|
||||||
readonly property bool rememberLastUser: _envRememberLastUser !== undefined ? _envRememberLastUser : SettingsData.greeterRememberLastUser
|
|
||||||
|
|
||||||
function setConfigBaseDir(dir) {
|
|
||||||
SettingsData.setGreeterSettingsBaseDir(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetConfigBaseDir() {
|
|
||||||
SettingsData.resetGreeterSettingsBaseDir();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string passwordBuffer: ""
|
|
||||||
property string username: ""
|
|
||||||
property string usernameInput: ""
|
|
||||||
property bool showPasswordInput: false
|
|
||||||
property string selectedSession: ""
|
|
||||||
property string selectedSessionPath: ""
|
|
||||||
property string selectedSessionDesktopId: ""
|
|
||||||
property string pamState: ""
|
|
||||||
property bool unlocking: false
|
|
||||||
|
|
||||||
property var sessionList: []
|
|
||||||
property var sessionExecs: []
|
|
||||||
property var sessionPaths: []
|
|
||||||
property var sessionDesktopIds: []
|
|
||||||
property int currentSessionIndex: 0
|
|
||||||
property bool sessionManuallySelected: false
|
|
||||||
property var availableUsers: []
|
|
||||||
property int selectedUserIndex: -1
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
showPasswordInput = false;
|
|
||||||
username = "";
|
|
||||||
usernameInput = "";
|
|
||||||
passwordBuffer = "";
|
|
||||||
pamState = "";
|
|
||||||
selectedUserIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Variants {
|
|
||||||
model: Quickshell.screens
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var modelData
|
|
||||||
|
|
||||||
screen: modelData
|
|
||||||
anchors {
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
top: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
exclusionMode: ExclusionMode.Normal
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
GreeterContent {
|
|
||||||
anchors.fill: parent
|
|
||||||
screenName: root.screen?.name ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.DCommon.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
LayoutMirroring.enabled: I18n.isRtl
|
|
||||||
LayoutMirroring.childrenInherit: true
|
|
||||||
|
|
||||||
property bool expanded: false
|
|
||||||
property int maxExpandedHeight: 400
|
|
||||||
property bool autoLoginVisible: false
|
|
||||||
property bool autoLoginChecked: false
|
|
||||||
property bool manualEntryVisible: false
|
|
||||||
|
|
||||||
signal userSelected(string username)
|
|
||||||
signal toggleRequested
|
|
||||||
signal autoLoginToggled
|
|
||||||
signal manualEntryRequested
|
|
||||||
|
|
||||||
readonly property int rowHeight: 52
|
|
||||||
readonly property int collapsedBarHeight: 36
|
|
||||||
readonly property int actionRowHeight: 44
|
|
||||||
|
|
||||||
readonly property int userListFullHeight: {
|
|
||||||
const count = GreeterUsersService.users.length;
|
|
||||||
if (count === 0)
|
|
||||||
return 0;
|
|
||||||
return count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS;
|
|
||||||
}
|
|
||||||
readonly property int manualEntryBlockHeight: manualEntryVisible ? actionRowHeight + Theme.spacingXS : 0
|
|
||||||
readonly property int autoLoginBlockHeight: autoLoginVisible ? actionRowHeight + Theme.spacingXS : 0
|
|
||||||
readonly property int expandedContentHeight: {
|
|
||||||
if (!expanded)
|
|
||||||
return 0;
|
|
||||||
if (GreeterUsersService.users.length === 0 && !autoLoginVisible && !manualEntryVisible)
|
|
||||||
return 0;
|
|
||||||
return Math.min(maxExpandedHeight, userListFullHeight + manualEntryBlockHeight + autoLoginBlockHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeFileUrl(path) {
|
|
||||||
if (!path)
|
|
||||||
return "";
|
|
||||||
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function profileImageSource(username) {
|
|
||||||
const path = GreeterUsersService.profileImagePath(username);
|
|
||||||
if (path)
|
|
||||||
return encodeFileUrl(path);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: expanded ? expandedContentHeight : collapsedBarHeight
|
|
||||||
implicitWidth: parent ? parent.width : 320
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
height: collapsedBarHeight
|
|
||||||
visible: !expanded
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: GreeterState.username ? GreeterUsersService.optionLabel(GreeterState.username) : I18n.tr("Select user...", "greeter user picker placeholder")
|
|
||||||
color: GreeterState.username ? Theme.surfaceText : Theme.surfaceVariantText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
DIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: "expand_more"
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.toggleRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
height: root.expandedContentHeight
|
|
||||||
visible: expanded
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DListView {
|
|
||||||
id: userListView
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - root.manualEntryBlockHeight - root.autoLoginBlockHeight
|
|
||||||
clip: true
|
|
||||||
interactive: contentHeight > height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
model: GreeterUsersService.users
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
id: userRow
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: userListView.width
|
|
||||||
height: root.rowHeight
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: userRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
|
||||||
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.preferredWidth: 36
|
|
||||||
Layout.preferredHeight: 36
|
|
||||||
|
|
||||||
DCircularImage {
|
|
||||||
anchors.fill: parent
|
|
||||||
ringWidth: Theme.avatarRingWidth
|
|
||||||
ringColor: Theme.avatarRingColor
|
|
||||||
imageSource: root.profileImageSource(userRow.modelData.username)
|
|
||||||
fallbackIcon: "material:person"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: GreeterUsersService.optionLabel(userRow.modelData.username)
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: userRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.userSelected(userRow.modelData.username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.actionRowHeight
|
|
||||||
visible: root.manualEntryVisible
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: manualEntryRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: "person_add"
|
|
||||||
size: 20
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: I18n.tr("Not listed?", "greeter link to switch to manual username entry")
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: manualEntryRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.manualEntryRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.actionRowHeight
|
|
||||||
visible: root.autoLoginVisible
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: autoLoginRowMouse.containsMouse ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DIcon {
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
name: root.autoLoginChecked ? "check_box" : "check_box_outline_blank"
|
|
||||||
size: 20
|
|
||||||
color: root.autoLoginChecked ? Theme.primary : Theme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: I18n.tr("Auto-login")
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: autoLoginRowMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.autoLoginToggled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var log: Log.scoped("GreeterUserTheme")
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
|
|
||||||
property string activeUsername: ""
|
|
||||||
|
|
||||||
function userCacheDir(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return greetCfgDir + "/users/" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyForUser(username) {
|
|
||||||
const name = (username || "").trim();
|
|
||||||
activeUsername = name;
|
|
||||||
if (!name) {
|
|
||||||
applyDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dir = userCacheDir(name);
|
|
||||||
if (typeof GreeterUsersService !== "undefined" && GreeterUsersService.hasSyncedTheme(name)) {
|
|
||||||
Theme.setGreeterColorsBaseDir(dir);
|
|
||||||
SessionData.setGreeterSessionBaseDir(dir);
|
|
||||||
GreetdSettings.setConfigBaseDir(dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDefault() {
|
|
||||||
activeUsername = "";
|
|
||||||
Theme.resetGreeterColorsBaseDir();
|
|
||||||
SessionData.resetGreeterSessionBaseDir();
|
|
||||||
GreetdSettings.resetConfigBaseDir();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Embedded Greetd module
|
|
||||||
|
|
||||||
TODO: remove when dank-greeter is available in the Arch extra repos.
|
|
||||||
|
|
||||||
The greeter moved to [dank-greeter](https://github.com/duxlabs-dev/dux-greeter). This embedded copy exists only for archinstall compatibility: the `niri - Dux Shell` archinstall profile writes an `/etc/greetd/config.toml` that launches `Modules/Greetd/assets/dms-greeter` from the packaged DMS tree, and dank-greeter is not installable from the official Arch repos yet.
|
|
||||||
|
|
||||||
Do not extend this module. New greeter work goes to dank-greeter. Installing greetd-dms-greeter-bin from the AUR and running `dms-greeter sync` migrates a system off this embedded copy.
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
COMPOSITOR=""
|
|
||||||
COMPOSITOR_CONFIG=""
|
|
||||||
DMS_PATH="dms-greeter"
|
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
|
||||||
REMEMBER_LAST_SESSION=""
|
|
||||||
REMEMBER_LAST_USER=""
|
|
||||||
DEBUG_MODE=0
|
|
||||||
|
|
||||||
show_help() {
|
|
||||||
cat << EOF
|
|
||||||
dms-greeter - Dux Shell greeter launcher
|
|
||||||
|
|
||||||
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, miracle, mango, or labwc)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-C, --config PATH Custom compositor config file
|
|
||||||
-p, --path PATH DMS path (config name or absolute path)
|
|
||||||
(default: dms-greeter)
|
|
||||||
--cache-dir PATH Cache directory for greeter data
|
|
||||||
(default: /var/cache/dms-greeter)
|
|
||||||
--remember-last-session BOOL
|
|
||||||
Persist selected session to greeter memory
|
|
||||||
(BOOL: true/false, default: from settings.json)
|
|
||||||
--remember-last-user BOOL
|
|
||||||
Persist last successful username to greeter memory
|
|
||||||
(BOOL: true/false, default: from settings.json)
|
|
||||||
--no-save-session Alias for --remember-last-session false
|
|
||||||
--no-save-username Alias for --remember-last-user false
|
|
||||||
--debug Enable verbose startup logging to stderr
|
|
||||||
-h, --help Show this help message
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
dms-greeter --command niri
|
|
||||||
dms-greeter --command hyprland -C /etc/greetd/custom-hypr.lua
|
|
||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
|
||||||
dms-greeter --command niri --no-save-session --no-save-username
|
|
||||||
dms-greeter --command mango
|
|
||||||
dms-greeter --command labwc
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local cmd="$1"
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
echo "Error: required command '$cmd' was not found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_bool_flag() {
|
|
||||||
local flag_name="$1"
|
|
||||||
local value="$2"
|
|
||||||
local normalized="${value,,}"
|
|
||||||
|
|
||||||
case "$normalized" in
|
|
||||||
1|true|yes|on)
|
|
||||||
echo "1"
|
|
||||||
;;
|
|
||||||
0|false|no|off)
|
|
||||||
echo "0"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_hyprland_lua_config() {
|
|
||||||
local config_file="$1"
|
|
||||||
case "$config_file" in
|
|
||||||
*.lua)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
if [[ -f "$config_file" ]] && grep -qE '(^|[^[:alnum:]_])hl\.' "$config_file"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
clear_vt() {
|
|
||||||
local clear_sequence=$'\033[2J\033[H\033[3J\033[?25l'
|
|
||||||
|
|
||||||
# Clear retained console text on the controlling VT.
|
|
||||||
if printf '%s' "$clear_sequence" >/dev/tty 2>/dev/null; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
printf '%s' "$clear_sequence"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
exec_compositor() {
|
|
||||||
local log_tag="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if [[ "$DEBUG_MODE" == "1" ]]; then
|
|
||||||
exec "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
clear_vt
|
|
||||||
|
|
||||||
if command -v systemd-cat >/dev/null 2>&1; then
|
|
||||||
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info 2>/dev/null) 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Preserve diagnostics on systems without journald.
|
|
||||||
exec "$@" >>"$CACHE_DIR/$log_tag.log" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--command)
|
|
||||||
COMPOSITOR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-C|--config)
|
|
||||||
COMPOSITOR_CONFIG="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-p|--path)
|
|
||||||
DMS_PATH="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--cache-dir)
|
|
||||||
CACHE_DIR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--remember-last-session)
|
|
||||||
REMEMBER_LAST_SESSION="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--remember-last-user)
|
|
||||||
REMEMBER_LAST_USER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--no-save-session)
|
|
||||||
REMEMBER_LAST_SESSION="0"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--no-save-username)
|
|
||||||
REMEMBER_LAST_USER="0"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--debug)
|
|
||||||
DEBUG_MODE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
show_help
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1" >&2
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$COMPOSITOR" ]]; then
|
|
||||||
echo "Error: --command COMPOSITOR is required" >&2
|
|
||||||
show_help
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
locate_dms_config() {
|
|
||||||
local config_name="$1"
|
|
||||||
local search_paths=()
|
|
||||||
|
|
||||||
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
||||||
search_paths+=("$config_home/quickshell/$config_name")
|
|
||||||
|
|
||||||
search_paths+=("/usr/share/quickshell/$config_name")
|
|
||||||
|
|
||||||
local config_dirs="${XDG_CONFIG_DIRS:-/etc/xdg}"
|
|
||||||
IFS=':' read -ra dirs <<< "$config_dirs"
|
|
||||||
for dir in "${dirs[@]}"; do
|
|
||||||
if [[ -n "$dir" ]]; then
|
|
||||||
search_paths+=("$dir/quickshell/$config_name")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for path in "${search_paths[@]}"; do
|
|
||||||
if [[ -f "$path/shell.qml" ]]; then
|
|
||||||
echo "$path"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
export DMS_RUN_GREETER=1
|
|
||||||
|
|
||||||
if [[ ! -d "$CACHE_DIR" ]]; then
|
|
||||||
echo "Error: cache directory '$CACHE_DIR' does not exist." >&2
|
|
||||||
echo " Run 'dms greeter sync' to initialize it, or pass --cache-dir to an existing directory." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
|
||||||
|
|
||||||
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
|
||||||
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
|
||||||
export DMS_GREET_REMEMBER_LAST_SESSION
|
|
||||||
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
|
||||||
DMS_SAVE_SESSION=true
|
|
||||||
else
|
|
||||||
DMS_SAVE_SESSION=false
|
|
||||||
fi
|
|
||||||
export DMS_SAVE_SESSION
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
|
||||||
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
|
||||||
export DMS_GREET_REMEMBER_LAST_USER
|
|
||||||
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
|
||||||
DMS_SAVE_USERNAME=true
|
|
||||||
else
|
|
||||||
DMS_SAVE_USERNAME=false
|
|
||||||
fi
|
|
||||||
export DMS_SAVE_USERNAME
|
|
||||||
fi
|
|
||||||
|
|
||||||
export HOME="$CACHE_DIR"
|
|
||||||
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
|
||||||
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
|
||||||
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
|
||||||
|
|
||||||
# Fallback runtime dir for systems without logind/pam_rundir (e.g. Void+seatd),
|
|
||||||
# where Wayland compositors abort with "RuntimeDirNotSet". Guarded so logind
|
|
||||||
# systems keep their own.
|
|
||||||
if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -d "${XDG_RUNTIME_DIR:-}" ]]; then
|
|
||||||
export XDG_RUNTIME_DIR="$CACHE_DIR/run"
|
|
||||||
mkdir -p "$XDG_RUNTIME_DIR"
|
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Keep greeter VT clean by default; callers can override via env or --debug.
|
|
||||||
if [[ -z "${RUST_LOG:-}" ]]; then
|
|
||||||
export RUST_LOG=warn
|
|
||||||
fi
|
|
||||||
if [[ -z "${NIRI_LOG:-}" ]]; then
|
|
||||||
export NIRI_LOG=warn
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export the user's configured cursor (from synced settings.json) for the
|
|
||||||
# compositor and greeter shell, but only if that theme is installed system-wide.
|
|
||||||
apply_user_cursor() {
|
|
||||||
command -v jq >/dev/null 2>&1 || return 0
|
|
||||||
local settings="$DMS_GREET_CFG_DIR/settings.json"
|
|
||||||
[[ -f "$settings" ]] || return 0
|
|
||||||
|
|
||||||
local theme size
|
|
||||||
theme=$(jq -r '.cursorSettings.theme // empty' "$settings" 2>/dev/null)
|
|
||||||
size=$(jq -r '.cursorSettings.size // empty' "$settings" 2>/dev/null)
|
|
||||||
[[ -n "$theme" && "$theme" != "System Default" ]] || return 0
|
|
||||||
|
|
||||||
local dirs=()
|
|
||||||
IFS=':' read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
|
||||||
local d
|
|
||||||
for d in "${data_dirs[@]}"; do
|
|
||||||
[[ -n "$d" ]] && dirs+=("$d/icons")
|
|
||||||
done
|
|
||||||
dirs+=(/run/current-system/sw/share/icons /usr/share/icons /usr/local/share/icons)
|
|
||||||
|
|
||||||
for d in "${dirs[@]}"; do
|
|
||||||
if [[ -d "$d/$theme/cursors" ]]; then
|
|
||||||
export XCURSOR_PATH="$d${XCURSOR_PATH:+:$XCURSOR_PATH}"
|
|
||||||
export XCURSOR_THEME="$theme"
|
|
||||||
[[ -n "$size" ]] && export XCURSOR_SIZE="$size"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
apply_user_cursor
|
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
|
||||||
QS_BIN="qs"
|
|
||||||
elif command -v quickshell >/dev/null 2>&1; then
|
|
||||||
QS_BIN="quickshell"
|
|
||||||
else
|
|
||||||
echo "Error: neither 'qs' nor 'quickshell' was found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
QS_CMD="$QS_BIN"
|
|
||||||
if [[ "$DMS_PATH" == /* ]]; then
|
|
||||||
QS_CMD="$QS_BIN -p $DMS_PATH"
|
|
||||||
else
|
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
|
||||||
if [[ "$DEBUG_MODE" == "1" ]]; then
|
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
|
||||||
fi
|
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
|
||||||
else
|
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$COMPOSITOR" in
|
|
||||||
niri)
|
|
||||||
require_command "niri"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
# base/default config
|
|
||||||
cat > "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
NIRI_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
# Append includes if override_files exist
|
|
||||||
OVERRIDE_FILES=(
|
|
||||||
/usr/share/greetd/niri_overrides.kdl # for building into a system image
|
|
||||||
/etc/greetd/niri_overrides.kdl # for local overrides
|
|
||||||
)
|
|
||||||
for override_file in "${OVERRIDE_FILES[@]}"; do
|
|
||||||
if [[ -e "$override_file" ]]; then
|
|
||||||
# TODO: maybe move to optional=true when Niri Next drops, so we can avoid these checks ;-)
|
|
||||||
cat >> "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
|
|
||||||
include "$override_file"
|
|
||||||
NIRI_EOF
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
# Append spawn command
|
|
||||||
cat >> "$TEMP_CONFIG" << NIRI_EOF
|
|
||||||
|
|
||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
|
||||||
NIRI_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
hyprland)
|
|
||||||
if ! command -v start-hyprland >/dev/null 2>&1 && ! command -v Hyprland >/dev/null 2>&1; then
|
|
||||||
echo "Error: neither 'start-hyprland' nor 'Hyprland' was found in PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]] && is_hyprland_lua_config "$COMPOSITOR_CONFIG"; then
|
|
||||||
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd("sh -c \"$QS_CMD; hyprctl dispatch 'hl.dsp.exit()'\"")
|
|
||||||
end)
|
|
||||||
HYPRLAND_LUA_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
|
||||||
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
|
||||||
hl.env("DMS_RUN_GREETER", "1")
|
|
||||||
|
|
||||||
hl.config({
|
|
||||||
misc = {
|
|
||||||
disable_hyprland_logo = true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd("sh -c \"$QS_CMD; hyprctl dispatch 'hl.dsp.exit()'\"")
|
|
||||||
end)
|
|
||||||
HYPRLAND_LUA_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
# hyprlang stays usable until 0.57 ships; legacy dispatch is the only form a .conf session accepts
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
|
|
||||||
|
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
|
||||||
HYPRLAND_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
|
||||||
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
|
||||||
else
|
|
||||||
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
sway)
|
|
||||||
require_command "sway"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << SWAY_EOF
|
|
||||||
exec "$QS_CMD; swaymsg exit"
|
|
||||||
SWAY_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << SWAY_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; swaymsg exit"
|
|
||||||
SWAY_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
scroll)
|
|
||||||
require_command "scroll"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << SCROLL_EOF
|
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
|
||||||
SCROLL_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << SCROLL_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; scrollmsg exit"
|
|
||||||
SCROLL_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
miracle|miracle-wm)
|
|
||||||
require_command "miracle-wm"
|
|
||||||
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat > "$TEMP_CONFIG" << MIRACLE_EOF
|
|
||||||
exec "$QS_CMD; miraclemsg exit"
|
|
||||||
MIRACLE_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
else
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
|
|
||||||
cat >> "$TEMP_CONFIG" << MIRACLE_EOF
|
|
||||||
|
|
||||||
exec "$QS_CMD; miraclemsg exit"
|
|
||||||
MIRACLE_EOF
|
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
|
||||||
fi
|
|
||||||
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
|
||||||
;;
|
|
||||||
|
|
||||||
labwc)
|
|
||||||
require_command "labwc"
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
|
||||||
else
|
|
||||||
exec_compositor "labwc" labwc --session "$QS_CMD"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
mango|mangowc)
|
|
||||||
require_command "mango"
|
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
|
||||||
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
|
|
||||||
else
|
|
||||||
# Without -c mango falls back to /etc/mango/config.conf, whose spawn/quit binds work at the login screen.
|
|
||||||
TEMP_CONFIG=$(mktemp)
|
|
||||||
exec_compositor "mango" mango -c "$TEMP_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
|
|
||||||
echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
|
|
||||||
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
|
|
||||||
|
|
||||||
hl.env("DMS_RUN_GREETER", "1")
|
|
||||||
|
|
||||||
hl.on("hyprland.start", function()
|
|
||||||
hl.exec_cmd("sh -c \"qs -p _DMS_PATH_; hyprctl dispatch 'hl.dsp.exit()'\"")
|
|
||||||
end)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
|
||||||
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
|
|
||||||
else
|
|
||||||
exec Hyprland -c /etc/greetd/dms-hypr.lua
|
|
||||||
fi
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export XDG_SESSION_TYPE=wayland
|
|
||||||
export QT_QPA_PLATFORM=wayland
|
|
||||||
export EGL_PLATFORM=gbm
|
|
||||||
|
|
||||||
exec niri -c /etc/greetd/dms-niri.kdl
|
|
||||||
@@ -220,6 +220,8 @@ Scope {
|
|||||||
|
|
||||||
function onSessionUnlocked() {
|
function onSessionUnlocked() {
|
||||||
customLockerSpawned = false;
|
customLockerSpawned = false;
|
||||||
|
if (AdminPolicy.disableLogindUnlock)
|
||||||
|
return;
|
||||||
if (pendingLock) {
|
if (pendingLock) {
|
||||||
pendingLock = false;
|
pendingLock = false;
|
||||||
lockInitiatedLocally = false;
|
lockInitiatedLocally = false;
|
||||||
@@ -412,14 +414,20 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unlock() {
|
function unlock() {
|
||||||
|
if (AdminPolicy.disableLockIpcUnlock)
|
||||||
|
return;
|
||||||
root.unlock();
|
root.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceReset() {
|
function forceReset() {
|
||||||
|
if (AdminPolicy.disableLockIpcUnlock)
|
||||||
|
return;
|
||||||
root.forceReset();
|
root.forceReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function demo() {
|
function demo() {
|
||||||
|
if (AdminPolicy.disableLockIpcUnlock)
|
||||||
|
return;
|
||||||
demoWindow.showDemo();
|
demoWindow.showDemo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import qs.Services
|
|||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property var log: Log.scoped("Pam")
|
||||||
|
|
||||||
property bool lockSecured: false
|
property bool lockSecured: false
|
||||||
property bool unlockInProgress: false
|
property bool unlockInProgress: false
|
||||||
|
|
||||||
@@ -96,6 +98,8 @@ Scope {
|
|||||||
if (!root.u2fSuppressedByPrimaryPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
|
if (!root.u2fSuppressedByPrimaryPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
|
||||||
u2f.startForSecondFactor();
|
u2f.startForSecondFactor();
|
||||||
} else {
|
} else {
|
||||||
|
if (AdminPolicy.disableCustomLockPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && !u2fConfigWatcher.loaded)
|
||||||
|
log.warn("/etc/pam.d/dankshell-u2f is missing, unlocking without the security key");
|
||||||
completeUnlock();
|
completeUnlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,8 +117,8 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
|
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
|
||||||
readonly property bool fprintSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineFprint)
|
readonly property bool fprintSuppressedByPrimaryPam: !AdminPolicy.disableCustomLockPam && (SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineFprint))
|
||||||
readonly property bool u2fSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineU2f)
|
readonly property bool u2fSuppressedByPrimaryPam: !AdminPolicy.disableCustomLockPam && (SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineU2f))
|
||||||
readonly property bool customU2fPamActive: SettingsData.lockU2fPamPath !== "" && customU2fPamWatcher.loaded
|
readonly property bool customU2fPamActive: SettingsData.lockU2fPamPath !== "" && customU2fPamWatcher.loaded
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -139,6 +143,13 @@ Scope {
|
|||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: fprintConfigWatcher
|
||||||
|
|
||||||
|
path: AdminPolicy.disableCustomLockPam ? "/etc/pam.d/dankshell-fprint" : ""
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: customU2fPamWatcher
|
id: customU2fPamWatcher
|
||||||
|
|
||||||
@@ -169,7 +180,7 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureUserPamConfig(): void {
|
function ensureUserPamConfig(): void {
|
||||||
if (SettingsData.lockPamExternallyManaged || resolveUserPam.running)
|
if (AdminPolicy.disableCustomLockPam || SettingsData.lockPamExternallyManaged || resolveUserPam.running)
|
||||||
return;
|
return;
|
||||||
resolveUserPam.running = true;
|
resolveUserPam.running = true;
|
||||||
}
|
}
|
||||||
@@ -180,6 +191,8 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return dankshellConfigWatcher.loaded ? "dankshell" : "login";
|
||||||
if (root.customPamActive)
|
if (root.customPamActive)
|
||||||
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
|
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
|
||||||
if (SettingsData.lockPamExternallyManaged)
|
if (SettingsData.lockPamExternallyManaged)
|
||||||
@@ -191,6 +204,8 @@ Scope {
|
|||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
configDirectory: {
|
configDirectory: {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return "/etc/pam.d";
|
||||||
if (root.customPamActive) {
|
if (root.customPamActive) {
|
||||||
const idx = SettingsData.lockPamPath.lastIndexOf("/");
|
const idx = SettingsData.lockPamPath.lastIndexOf("/");
|
||||||
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
|
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
|
||||||
@@ -299,14 +314,15 @@ Scope {
|
|||||||
PamContext {
|
PamContext {
|
||||||
id: fprint
|
id: fprint
|
||||||
|
|
||||||
readonly property bool available: SettingsData.lockFingerprintReady
|
readonly property bool available: SettingsData.lockFingerprintReady && (!AdminPolicy.disableCustomLockPam || fprintConfigWatcher.loaded)
|
||||||
property int tries: 0
|
property int tries: 0
|
||||||
property int errorTries: 0
|
property int errorTries: 0
|
||||||
property double attemptStartedAt: 0
|
property double attemptStartedAt: 0
|
||||||
property bool completedDuringStart: false
|
property bool completedDuringStart: false
|
||||||
readonly property int maxErrorTries: 200
|
readonly property int maxErrorTries: 200
|
||||||
readonly property int daemonIdleExitMs: 30000
|
readonly property int daemonIdleExitMs: 30000
|
||||||
// sessionTimeoutMs mirrors `timeout=` in assets/pam/fprint
|
// sessionTimeoutMs mirrors `timeout=` in assets/pam/fprint; under the
|
||||||
|
// policy, /etc/pam.d/dankshell-fprint must carry the same value
|
||||||
readonly property int sessionTimeoutMs: 90000
|
readonly property int sessionTimeoutMs: 90000
|
||||||
readonly property int verifyStartSlackMs: 10000
|
readonly property int verifyStartSlackMs: 10000
|
||||||
readonly property bool retrying: errorRetry.running
|
readonly property bool retrying: errorRetry.running
|
||||||
@@ -401,8 +417,8 @@ Scope {
|
|||||||
Qt.callLater(checkAvail);
|
Qt.callLater(checkAvail);
|
||||||
}
|
}
|
||||||
|
|
||||||
config: "fprint"
|
config: AdminPolicy.disableCustomLockPam ? "dankshell-fprint" : "fprint"
|
||||||
configDirectory: Quickshell.shellDir + "/assets/pam"
|
configDirectory: AdminPolicy.disableCustomLockPam ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onPamMessage: {
|
onPamMessage: {
|
||||||
if (attemptStartedAt === 0)
|
if (attemptStartedAt === 0)
|
||||||
@@ -446,7 +462,7 @@ Scope {
|
|||||||
PamContext {
|
PamContext {
|
||||||
id: u2f
|
id: u2f
|
||||||
|
|
||||||
property bool available: SettingsData.lockU2fReady
|
property bool available: SettingsData.lockU2fReady && (!AdminPolicy.disableCustomLockPam || u2fConfigWatcher.loaded)
|
||||||
|
|
||||||
function checkAvail(): void {
|
function checkAvail(): void {
|
||||||
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByPrimaryPam) {
|
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByPrimaryPam) {
|
||||||
@@ -483,11 +499,15 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return "dankshell-u2f";
|
||||||
if (root.customU2fPamActive)
|
if (root.customU2fPamActive)
|
||||||
return SettingsData.lockU2fPamPath.slice(SettingsData.lockU2fPamPath.lastIndexOf("/") + 1);
|
return SettingsData.lockU2fPamPath.slice(SettingsData.lockU2fPamPath.lastIndexOf("/") + 1);
|
||||||
return u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f";
|
return u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f";
|
||||||
}
|
}
|
||||||
configDirectory: {
|
configDirectory: {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return "/etc/pam.d";
|
||||||
if (root.customU2fPamActive) {
|
if (root.customU2fPamActive) {
|
||||||
const idx = SettingsData.lockU2fPamPath.lastIndexOf("/");
|
const idx = SettingsData.lockU2fPamPath.lastIndexOf("/");
|
||||||
return idx > 0 ? SettingsData.lockU2fPamPath.slice(0, idx) : "/";
|
return idx > 0 ? SettingsData.lockU2fPamPath.slice(0, idx) : "/";
|
||||||
@@ -694,6 +714,8 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onLockPamExternallyManagedChanged(): void {
|
function onLockPamExternallyManagedChanged(): void {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return;
|
||||||
root.resetAuthFlows();
|
root.resetAuthFlows();
|
||||||
if (!SettingsData.lockPamExternallyManaged)
|
if (!SettingsData.lockPamExternallyManaged)
|
||||||
root.ensureUserPamConfig();
|
root.ensureUserPamConfig();
|
||||||
@@ -702,6 +724,8 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onLockU2fPamPathChanged(): void {
|
function onLockU2fPamPathChanged(): void {
|
||||||
|
if (AdminPolicy.disableCustomLockPam)
|
||||||
|
return;
|
||||||
u2f.abort();
|
u2f.abort();
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Rectangle {
|
|||||||
|
|
||||||
function saveDisplayImage() {
|
function saveDisplayImage() {
|
||||||
const data = root.notificationData;
|
const data = root.notificationData;
|
||||||
if (!root.persistImage || !data?.hasDisplayImage || data.persistedImagePath)
|
if (!root.persistImage || !data?.notification || !data.hasDisplayImage || data.persistedImagePath)
|
||||||
return;
|
return;
|
||||||
const image = data.image || "";
|
const image = data.image || "";
|
||||||
if (!image.startsWith("image://qsimage/") && !NotificationService.notificationIconFromImage(image).startsWith("/"))
|
if (!image.startsWith("image://qsimage/") && !NotificationService.notificationIconFromImage(image).startsWith("/"))
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ PanelWindow {
|
|||||||
readonly property bool cardShown: presenting && (presentationProgress > 0 || !exiting)
|
readonly property bool cardShown: presenting && (presentationProgress > 0 || !exiting)
|
||||||
readonly property bool layoutPinned: !exiting && (hovered || contextMenuActive || swipeActive)
|
readonly property bool layoutPinned: !exiting && (hovered || contextMenuActive || swipeActive)
|
||||||
property bool exiting: false
|
property bool exiting: false
|
||||||
|
property var exitSnapshot: null
|
||||||
property bool exitStarted: false
|
property bool exitStarted: false
|
||||||
property bool collapseChromeOnExit: false
|
property bool collapseChromeOnExit: false
|
||||||
property bool _isDestroying: false
|
property bool _isDestroying: false
|
||||||
@@ -124,6 +125,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
enterAnimation.stop();
|
enterAnimation.stop();
|
||||||
enterDelay.stop();
|
enterDelay.stop();
|
||||||
|
exitSnapshot = snapshotNotification(notificationData);
|
||||||
exiting = true;
|
exiting = true;
|
||||||
if (NotificationService.removeFromVisibleNotifications)
|
if (NotificationService.removeFromVisibleNotifications)
|
||||||
NotificationService.removeFromVisibleNotifications(win.notificationData);
|
NotificationService.removeFromVisibleNotifications(win.notificationData);
|
||||||
@@ -172,6 +174,29 @@ PanelWindow {
|
|||||||
win.exitFinished();
|
win.exitFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The wrapper is destroyed as soon as its notification closes, which would blank the card mid-exit.
|
||||||
|
function snapshotNotification(data) {
|
||||||
|
if (!data)
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
"appName": data.appName,
|
||||||
|
"appIcon": data.appIcon,
|
||||||
|
"desktopEntry": data.desktopEntry,
|
||||||
|
"summary": data.summary,
|
||||||
|
"body": data.body,
|
||||||
|
"htmlBody": data.htmlBody,
|
||||||
|
"image": data.image,
|
||||||
|
"hasDisplayImage": data.hasDisplayImage,
|
||||||
|
"displayImage": data.displayImage,
|
||||||
|
"persistedImagePath": data.persistedImagePath,
|
||||||
|
"timeStr": data.timeStr,
|
||||||
|
"urgency": data.urgency,
|
||||||
|
"actions": Array.from(data.actions || [], action => ({
|
||||||
|
"text": action.text
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function closeTransientUi() {
|
function closeTransientUi() {
|
||||||
transientSurfaces.closeAll();
|
transientSurfaces.closeAll();
|
||||||
popupContextMenuLoader.active = false;
|
popupContextMenuLoader.active = false;
|
||||||
@@ -719,7 +744,7 @@ PanelWindow {
|
|||||||
chipColor: Theme.notificationChipSurface
|
chipColor: Theme.notificationChipSurface
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: win.inlineHeightAnimating ? Math.min(targetHeight, cardSurface.height - win.timeoutRailClearance) : targetHeight
|
height: win.inlineHeightAnimating ? Math.min(targetHeight, cardSurface.height - win.timeoutRailClearance) : targetHeight
|
||||||
notificationData: win.notificationData
|
notificationData: win.exitSnapshot ?? win.notificationData
|
||||||
descriptionExpanded: win.descriptionExpanded
|
descriptionExpanded: win.descriptionExpanded
|
||||||
privacyMode: SettingsData.notificationPopupPrivacyMode
|
privacyMode: SettingsData.notificationPopupPrivacyMode
|
||||||
bodyInvokesAction: win.bodyClickInvokesAction
|
bodyInvokesAction: win.bodyClickInvokesAction
|
||||||
|
|||||||
@@ -557,6 +557,13 @@ QtObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: _sync(NotificationService.visibleNotifications)
|
Component.onCompleted: _sync(NotificationService.visibleNotifications)
|
||||||
|
Component.onDestruction: {
|
||||||
|
for (const p of popupWindows)
|
||||||
|
p?.destroy();
|
||||||
|
const screenName = manager.modelData?.name || "";
|
||||||
|
if (screenName)
|
||||||
|
ConnectedModeState.releaseSurface(screenName, "notification", "notification:" + screenName);
|
||||||
|
}
|
||||||
onPopupSpacingChanged: _repositionAll()
|
onPopupSpacingChanged: _repositionAll()
|
||||||
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
|
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
|
||||||
onCloseGapNotificationsChanged: _scheduleNotificationChromeSync()
|
onCloseGapNotificationsChanged: _scheduleNotificationChromeSync()
|
||||||
|
|||||||
@@ -739,7 +739,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
tags: ["mangowc", "mango", "gaps", "override", "inner", "gappih", "gappiv"]
|
tags: ["mangowc", "mango", "gaps", "override", "inner", "gap_inner_horizontal", "gap_inner_vertical", "gappih", "gappiv"]
|
||||||
settingKey: "mangoLayoutGapsOverride"
|
settingKey: "mangoLayoutGapsOverride"
|
||||||
resetKeys: []
|
resetKeys: []
|
||||||
text: I18n.tr("Inner gaps")
|
text: I18n.tr("Inner gaps")
|
||||||
@@ -752,7 +752,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
tags: ["mangowc", "mango", "gaps", "override", "outer", "edge", "gappoh", "gappov"]
|
tags: ["mangowc", "mango", "gaps", "override", "outer", "edge", "gap_outer_horizontal", "gap_outer_vertical", "gappoh", "gappov"]
|
||||||
settingKey: "mangoLayoutGapsOutOverride"
|
settingKey: "mangoLayoutGapsOutOverride"
|
||||||
text: I18n.tr("Outer gaps")
|
text: I18n.tr("Outer gaps")
|
||||||
visible: SettingsData.mangoLayoutGapsOverride >= 0
|
visible: SettingsData.mangoLayoutGapsOverride >= 0
|
||||||
@@ -772,7 +772,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsToggleSliderRow {
|
SettingsToggleSliderRow {
|
||||||
tags: ["mangowc", "mango", "border", "override", "borderpx"]
|
tags: ["mangowc", "mango", "border", "override", "border_px", "borderpx"]
|
||||||
settingKey: "mangoLayoutBorderSizeEnabled"
|
settingKey: "mangoLayoutBorderSizeEnabled"
|
||||||
text: I18n.tr("Override border size")
|
text: I18n.tr("Override border size")
|
||||||
checked: SettingsData.mangoLayoutBorderSize >= 0
|
checked: SettingsData.mangoLayoutBorderSize >= 0
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Column {
|
|||||||
property var parentModal: null
|
property var parentModal: null
|
||||||
property string editingDockId: ""
|
property string editingDockId: ""
|
||||||
property string renameDraft: ""
|
property string renameDraft: ""
|
||||||
|
readonly property var dockPages: ["dock_general", "dock_widgets", "dock_appearance"].map(id => SettingsTabs.page(id)).filter(page => page)
|
||||||
|
|
||||||
function finishRename(value) {
|
function finishRename(value) {
|
||||||
const id = editingDockId;
|
const id = editingDockId;
|
||||||
@@ -44,11 +45,10 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
|
||||||
iconName: "dock_to_bottom"
|
iconName: "dock_to_bottom"
|
||||||
title: I18n.tr("Docks", "noun plural, settings card title listing configured docks")
|
title: I18n.tr("Docks", "noun plural, settings card title listing configured docks")
|
||||||
settingKey: "dockConfiguration"
|
settingKey: "dockConfiguration"
|
||||||
tags: ["dock", "configuration", "add", "remove", "name", "show", "enable"]
|
tags: ["dock", "configuration", "add", "remove", "show", "enable"]
|
||||||
headerActions: DButton {
|
headerActions: DButton {
|
||||||
text: I18n.tr("Add", "verb, button that adds a new item to a list")
|
text: I18n.tr("Add", "verb, button that adds a new item to a list")
|
||||||
iconName: "add"
|
iconName: "add"
|
||||||
@@ -75,11 +75,18 @@ Column {
|
|||||||
onDeleteRequested: root.removeDock(modelData.id)
|
onDeleteRequested: root.removeDock(modelData.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
iconName: "dock_to_bottom"
|
||||||
|
title: dock.config?.name ?? ""
|
||||||
|
settingKey: "dockPlacement"
|
||||||
|
tags: ["dock", "layout", "placement", "position", "edge", "name", "rename"]
|
||||||
|
visible: dock.hasConfig
|
||||||
|
|
||||||
SettingsRow {
|
SettingsRow {
|
||||||
title: I18n.tr("Name")
|
title: I18n.tr("Name")
|
||||||
subtitle: root.editingDockId ? "" : dock.config?.name ?? ""
|
subtitle: root.editingDockId ? "" : dock.config?.name ?? ""
|
||||||
visible: dock.hasConfig
|
|
||||||
|
|
||||||
DActionButton {
|
DActionButton {
|
||||||
iconName: root.editingDockId ? "check" : "edit"
|
iconName: root.editingDockId ? "check" : "edit"
|
||||||
@@ -120,25 +127,33 @@ Column {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsRow {
|
||||||
width: parent.width
|
title: I18n.tr("Layout", "noun, settings section title for arrangement options")
|
||||||
visible: dock.hasConfig
|
|
||||||
iconName: "dock_to_bottom"
|
|
||||||
title: I18n.tr("Layout")
|
|
||||||
settingKey: "dockPlacement"
|
|
||||||
tags: ["dock", "layout", "placement", "position", "edge"]
|
|
||||||
|
|
||||||
SettingsLayoutPicker {
|
body: SettingsLayoutPicker {
|
||||||
edgePlacement: true
|
edgePlacement: true
|
||||||
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
|
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
|
||||||
key: String(position),
|
key: String(position),
|
||||||
label: dock.positionLabel(position),
|
label: dock.positionLabel(position),
|
||||||
enabled: dock.positionChoices.includes(position)
|
enabled: dock.positionChoices.includes(position)
|
||||||
}))
|
}))
|
||||||
selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom)
|
selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom)
|
||||||
onSelected: key => dock.setEnabled(true, Number(key))
|
onSelected: key => dock.setEnabled(true, Number(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.dockPages
|
||||||
|
|
||||||
|
delegate: SettingsNavRow {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
iconName: modelData.icon
|
||||||
|
title: modelData.text
|
||||||
|
hint: modelData.hint ?? ""
|
||||||
|
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,11 @@ Item {
|
|||||||
property string greeterStatusStderr: ""
|
property string greeterStatusStderr: ""
|
||||||
readonly property bool greeterBinaryExists: GreeterService.binaryExists
|
readonly property bool greeterBinaryExists: GreeterService.binaryExists
|
||||||
readonly property bool greeterEnabled: GreeterService.enabled
|
readonly property bool greeterEnabled: GreeterService.enabled
|
||||||
property bool embeddedGreeterConfigured: false
|
|
||||||
readonly property bool embeddedGreeterOnly: embeddedGreeterConfigured && !greeterBinaryExists
|
|
||||||
readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning
|
readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning
|
||||||
|
|
||||||
readonly property string greeterState: {
|
readonly property string greeterState: {
|
||||||
if (!greeterBinaryExists)
|
if (!greeterBinaryExists)
|
||||||
return embeddedGreeterOnly ? "embedded" : "missing";
|
return "missing";
|
||||||
if (!greeterEnabled)
|
if (!greeterEnabled)
|
||||||
return "inactive";
|
return "inactive";
|
||||||
switch (GreeterService.slotState) {
|
switch (GreeterService.slotState) {
|
||||||
@@ -103,8 +101,6 @@ Item {
|
|||||||
return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter");
|
return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter");
|
||||||
case "unlinked":
|
case "unlinked":
|
||||||
return I18n.tr("Not linked", "greeter status, no greeter slot for this user");
|
return I18n.tr("Not linked", "greeter status, no greeter slot for this user");
|
||||||
case "embedded":
|
|
||||||
return I18n.tr("Bundled greeter active", "greeter status");
|
|
||||||
}
|
}
|
||||||
return I18n.tr("Not installed", "greeter status");
|
return I18n.tr("Not installed", "greeter status");
|
||||||
}
|
}
|
||||||
@@ -118,8 +114,6 @@ Item {
|
|||||||
return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status");
|
return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status");
|
||||||
case "unlinked":
|
case "unlinked":
|
||||||
return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status");
|
return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status");
|
||||||
case "embedded":
|
|
||||||
return I18n.tr("The greeter bundled with DMS is active (archinstall setup). It keeps working as is, but syncing theme and settings needs the standalone greeter. Install greetd-dms-greeter-bin from the AUR, then run Sync to migrate the login screen.", "embedded greeter status");
|
|
||||||
}
|
}
|
||||||
return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder");
|
return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder");
|
||||||
}
|
}
|
||||||
@@ -149,7 +143,6 @@ Item {
|
|||||||
|
|
||||||
function checkGreeterInstallState() {
|
function checkGreeterInstallState() {
|
||||||
GreeterService.refresh();
|
GreeterService.refresh();
|
||||||
embeddedGreeterCheckProcess.running = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function runGreeterStatus() {
|
function runGreeterStatus() {
|
||||||
@@ -205,17 +198,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: embeddedGreeterCheckProcess
|
|
||||||
// archinstall's DMS profile points greetd at this launcher inside the packaged DMS tree
|
|
||||||
command: ["sh", "-c", "grep -q 'Modules/Greetd/assets/dms-greeter' /etc/greetd/config.toml 2>/dev/null"]
|
|
||||||
running: false
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
root.embeddedGreeterConfigured = (exitCode === 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: greeterStatusProcess
|
id: greeterStatusProcess
|
||||||
command: ["dms-greeter", "status"]
|
command: ["dms-greeter", "status"]
|
||||||
|
|||||||
@@ -1577,7 +1577,9 @@ RegistryBrowserWindow {
|
|||||||
visible: (detailPane.plugin.permissions || []).length > 0
|
visible: (detailPane.plugin.permissions || []).length > 0
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("This plugin can", "plugin detail section, introduces the permission list")
|
width: parent.width
|
||||||
|
text: I18n.tr("Declared by the author, not enforced", "plugin detail section, introduces the permission list the plugin declares about itself")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Theme.fontWeightMedium
|
font.weight: Theme.fontWeightMedium
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -399,12 +399,16 @@ Column {
|
|||||||
|
|
||||||
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Scrolling", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Scrolling", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
||||||
readonly property var fillModeLabels: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Scroll", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile Vertically", "wallpaper fill mode"), I18n.tr("Tile Horizontally", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")]
|
readonly property var fillModeLabels: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Scroll", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile Vertically", "wallpaper fill mode"), I18n.tr("Tile Horizontally", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")]
|
||||||
|
// dms picks the animation driver at launch from this setting
|
||||||
|
readonly property bool simpleAnimationDriver: Quickshell.env("QSG_USE_SIMPLE_ANIMATION_DRIVER") === "1"
|
||||||
|
readonly property bool driverMismatch: (SettingsData.wallpaperFillMode === "Scrolling") !== simpleAnimationDriver
|
||||||
|
|
||||||
tab: "wallpaper"
|
tab: "wallpaper"
|
||||||
tags: ["background", "fill", "fit", "stretch", "tile", "scale"]
|
tags: ["background", "fill", "fit", "stretch", "tile", "scale"]
|
||||||
settingKey: "wallpaperFillMode"
|
settingKey: "wallpaperFillMode"
|
||||||
resetKeys: root.perMonitor ? [] : ["wallpaperFillMode"]
|
resetKeys: root.perMonitor ? [] : ["wallpaperFillMode"]
|
||||||
text: I18n.tr("Fill mode")
|
text: I18n.tr("Fill mode")
|
||||||
|
description: driverMismatch ? I18n.tr("Requires restart") : ""
|
||||||
visible: root.wallpaperIsImage
|
visible: root.wallpaperIsImage
|
||||||
dropdownWidth: 190
|
dropdownWidth: 190
|
||||||
options: fillModeLabels
|
options: fillModeLabels
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ SettingsDropdownRow {
|
|||||||
function _enumerate() {
|
function _enumerate() {
|
||||||
if (_enumerated)
|
if (_enumerated)
|
||||||
return;
|
return;
|
||||||
const bundled = DCommon.Fonts.bundledFamilies.filter(f => f !== root.defaultFamily);
|
_families = ["Default"].concat(DCommon.Fonts.selectableFamilies(root.defaultFamily));
|
||||||
const system = Qt.fontFamilies().filter(f => !f.startsWith(".") && f !== root.defaultFamily && !bundled.includes(f));
|
|
||||||
system.sort();
|
|
||||||
_families = ["Default"].concat(bundled, system);
|
|
||||||
_enumerated = true;
|
_enumerated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property var _blurRegisteredWindow: null
|
property var _blurRegisteredWindow: null
|
||||||
readonly property var _blurTargetWindow: BlurService.enabled && !!blurBarWindow && !!blurBarWindow.registerBlurWidget && !root.noBackground && root.visible && root.width > 0 ? blurBarWindow : null
|
readonly property var _blurTargetWindow: BlurService.enabled && !!blurBarWindow && !!blurBarWindow.registerBlurWidget && !root.noBackground && root.visible && root.width > 0 && root.height > 0 ? blurBarWindow : null
|
||||||
|
|
||||||
on_BlurTargetWindowChanged: _updateBlurRegistration()
|
on_BlurTargetWindowChanged: _updateBlurRegistration()
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Singleton {
|
|||||||
readonly property string markerPath: currentVersion !== "" ? Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/dms/.changelog-" + currentVersion : ""
|
readonly property string markerPath: currentVersion !== "" ? Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) + "/dms/.changelog-" + currentVersion : ""
|
||||||
property bool markerChecked: false
|
property bool markerChecked: false
|
||||||
property bool markerSeen: false
|
property bool markerSeen: false
|
||||||
readonly property bool ready: !SessionData.isGreeterMode && FirstLaunchService.checkComplete && markerChecked
|
readonly property bool ready: FirstLaunchService.checkComplete && markerChecked
|
||||||
|
|
||||||
property bool changelogDismissed: false
|
property bool changelogDismissed: false
|
||||||
property var release: null
|
property var release: null
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ Singleton {
|
|||||||
const yKey = y.niriWindowId !== undefined ? y.niriWindowId : y.mangoWindowId;
|
const yKey = y.niriWindowId !== undefined ? y.niriWindowId : y.mangoWindowId;
|
||||||
if (xKey === undefined || yKey === undefined)
|
if (xKey === undefined || yKey === undefined)
|
||||||
return false;
|
return false;
|
||||||
if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized)
|
if (xKey !== yKey || x.niriWorkspaceId !== y.niriWorkspaceId || x.mangoMonitor !== y.mangoMonitor || x.appId !== y.appId || x.title !== y.title || !!x.activated !== !!y.activated || !!x.fullscreen !== !!y.fullscreen || !!x.maximized !== !!y.maximized || !!x.minimized !== !!y.minimized)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property var log: Log.scoped("GreeterUsersService")
|
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
|
||||||
readonly property string usersCacheDir: greetCfgDir + "/users"
|
|
||||||
|
|
||||||
property var users: []
|
|
||||||
property var usernames: []
|
|
||||||
property var profileImageMap: ({})
|
|
||||||
property bool loaded: false
|
|
||||||
property bool refreshing: false
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (!SessionData.isGreeterMode)
|
|
||||||
return;
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
if (refreshing)
|
|
||||||
return;
|
|
||||||
refreshing = true;
|
|
||||||
_loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayName(username) {
|
|
||||||
const u = _findUser(username);
|
|
||||||
if (!u)
|
|
||||||
return username || "";
|
|
||||||
const gecos = (u.gecos || "").trim();
|
|
||||||
return gecos.length > 0 ? gecos : username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionLabel(username) {
|
|
||||||
const label = displayName(username);
|
|
||||||
return label !== username ? label : username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function usernameFromOptionLabel(label) {
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
if (root.optionLabel(users[i].username) === label)
|
|
||||||
return users[i].username;
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSyncedTheme(username) {
|
|
||||||
if (!username)
|
|
||||||
return false;
|
|
||||||
return syncedThemePaths[username] === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
property var syncedThemePaths: ({})
|
|
||||||
|
|
||||||
function userCacheDir(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return usersCacheDir + "/" + username;
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncedSettingsPath(username) {
|
|
||||||
const dir = userCacheDir(username);
|
|
||||||
return dir ? dir + "/settings.json" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _findUser(name) {
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
if (users[i].username === name)
|
|
||||||
return users[i];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadUsers() {
|
|
||||||
Proc.runCommand("greeterUsersService-loadUsers", ["sh", "-c", "getent passwd | awk -F: '$3>=1000 && $3<60000 && $1!=\"nobody\" && $7!~/(nologin|false)$/ && $6!=\"/var/empty\" {print $1\":\"$3\":\"$5\":\"$6\":\"$7}'"], (output, exitCode) => {
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
const list = [];
|
|
||||||
const names = [];
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length < 5)
|
|
||||||
continue;
|
|
||||||
const username = parts[0];
|
|
||||||
list.push({
|
|
||||||
username,
|
|
||||||
uid: parseInt(parts[1], 10),
|
|
||||||
gecos: (parts[2] || "").split(",")[0],
|
|
||||||
home: parts[3] || "",
|
|
||||||
shell: parts[4] || ""
|
|
||||||
});
|
|
||||||
names.push(username);
|
|
||||||
}
|
|
||||||
list.sort((a, b) => a.username.localeCompare(b.username));
|
|
||||||
names.sort((a, b) => a.localeCompare(b));
|
|
||||||
root.users = list;
|
|
||||||
root.usernames = names;
|
|
||||||
root.loaded = true;
|
|
||||||
root.refreshing = false;
|
|
||||||
_refreshSyncedThemeFlags();
|
|
||||||
_loadProfileIcons();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _refreshSyncedThemeFlags() {
|
|
||||||
if (usernames.length === 0) {
|
|
||||||
syncedThemePaths = ({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const checks = usernames.map(u => `[ -f "${syncedSettingsPath(u)}" ] && echo "${u}:1" || echo "${u}:0"`).join("; ");
|
|
||||||
Proc.runCommand("greeterUsersService-syncedThemes", ["sh", "-c", checks], (output, exitCode) => {
|
|
||||||
const map = {};
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const parts = lines[i].split(":");
|
|
||||||
if (parts.length >= 2)
|
|
||||||
map[parts[0]] = parts[1] === "1";
|
|
||||||
}
|
|
||||||
root.syncedThemePaths = map;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function profileImagePath(username) {
|
|
||||||
if (!username)
|
|
||||||
return "";
|
|
||||||
return profileImageMap[username] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loadProfileIcons() {
|
|
||||||
if (users.length === 0) {
|
|
||||||
profileImageMap = ({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const script = users.map(u => {
|
|
||||||
const safeUser = u.username.replace(/'/g, "'\\''");
|
|
||||||
const safeHome = (u.home || "").replace(/'/g, "'\\''");
|
|
||||||
const cacheDir = usersCacheDir + "/" + u.username;
|
|
||||||
return `( icon=""; for f in "${cacheDir}/profile.jpg" "${cacheDir}/profile.jpeg" "${cacheDir}/profile.png" "${cacheDir}/profile.webp" "/var/lib/AccountsService/icons/${safeUser}" "${safeHome}/.face" "${safeHome}/.face.icon"; do if [ -f "$f" ] && [ -r "$f" ]; then icon="$f"; break; fi; done; echo "${u.username}:$icon" )`;
|
|
||||||
}).join("; ");
|
|
||||||
Proc.runCommand("greeterUsersService-profileIcons", ["sh", "-c", script], (output, exitCode) => {
|
|
||||||
const map = {};
|
|
||||||
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const idx = lines[i].indexOf(":");
|
|
||||||
if (idx <= 0)
|
|
||||||
continue;
|
|
||||||
const user = lines[i].substring(0, idx);
|
|
||||||
const icon = lines[i].substring(idx + 1).trim();
|
|
||||||
map[user] = icon && icon.length > 0 ? icon : "";
|
|
||||||
}
|
|
||||||
for (let j = 0; j < users.length; j++) {
|
|
||||||
const u = users[j].username;
|
|
||||||
if (!(u in map))
|
|
||||||
map[u] = "";
|
|
||||||
}
|
|
||||||
root.profileImageMap = map;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1051,6 +1051,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDmsActions() {
|
function getDmsActions() {
|
||||||
return Actions.getDmsActions(CompositorService.isNiri, CompositorService.isHyprland);
|
const actions = Actions.getDmsActions(CompositorService.isNiri, CompositorService.isHyprland);
|
||||||
|
if (!AdminPolicy.disableLockIpcUnlock)
|
||||||
|
return actions;
|
||||||
|
return actions.filter(action => action.id !== "spawn dms ipc call lock demo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ QtObject {
|
|||||||
if (enabled && (state === "none" || state === "error"))
|
if (enabled && (state === "none" || state === "error"))
|
||||||
useEmbeddedText(state);
|
useEmbeddedText(state);
|
||||||
}
|
}
|
||||||
onPositionChanged: scheduleAnchor()
|
onPositionChanged: {
|
||||||
|
if (MprisController.refreshingPosition || flooredEcho())
|
||||||
|
return;
|
||||||
|
scheduleAnchor();
|
||||||
|
}
|
||||||
onRateChanged: updatePlaybackClock()
|
onRateChanged: updatePlaybackClock()
|
||||||
onPlayingChanged: {
|
onPlayingChanged: {
|
||||||
probe = null;
|
probe = null;
|
||||||
@@ -379,6 +383,7 @@ QtObject {
|
|||||||
if (!line || !Number.isFinite(line.t))
|
if (!line || !Number.isFinite(line.t))
|
||||||
return false;
|
return false;
|
||||||
player.position = Math.max(0.1, Math.min(line.t, duration * 0.99));
|
player.position = Math.max(0.1, Math.min(line.t, duration * 0.99));
|
||||||
|
reanchor();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +439,13 @@ QtObject {
|
|||||||
resync();
|
resync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pear-desktop floors Position to whole seconds on pause, resume and track events.
|
||||||
|
function flooredEcho() {
|
||||||
|
const lead = currentTime() - position;
|
||||||
|
const whole = Math.abs(position - Math.round(position)) < DashMetrics.mediaLyricsWholeSecondSlack;
|
||||||
|
return whole && lead >= 0 && lead < 1 + DashMetrics.mediaLyricsPositionTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleAnchor() {
|
function scheduleAnchor() {
|
||||||
positionWall = Date.now();
|
positionWall = Date.now();
|
||||||
if (enabled && synced && !stopped && !anchorUpdate.running)
|
if (enabled && synced && !stopped && !anchorUpdate.running)
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ Singleton {
|
|||||||
readonly property string inputPath: mangoDmsDir + "/input.conf"
|
readonly property string inputPath: mangoDmsDir + "/input.conf"
|
||||||
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
||||||
|
|
||||||
// Mango after 0.17.5 renamed every config key to snake_case with no aliases.
|
|
||||||
property bool snakeKeys: false
|
|
||||||
property bool dialectReady: false
|
|
||||||
property bool inputGenerationPending: false
|
property bool inputGenerationPending: false
|
||||||
property bool inputGenerationRunning: false
|
property bool inputGenerationRunning: false
|
||||||
property int _lastGapValue: -1
|
property int _lastGapValue: -1
|
||||||
@@ -587,21 +584,11 @@ Singleton {
|
|||||||
Connections {
|
Connections {
|
||||||
target: CompositorService
|
target: CompositorService
|
||||||
function onIsMangoChanged() {
|
function onIsMangoChanged() {
|
||||||
if (CompositorService.isMango)
|
if (!CompositorService.isMango)
|
||||||
detectDialect();
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also respells the DMS fragments for the installed Mango; an older dms keeps legacy keys.
|
|
||||||
function detectDialect() {
|
|
||||||
Proc.runCommand("mango-dialect", [Proc.dmsBin, "config", "mango-migrate"], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0)
|
|
||||||
log.warn("mango-migrate exited", exitCode, "- some fragments may keep the old key spelling");
|
|
||||||
root.snakeKeys = output.trim().split("\n").pop() === "snake";
|
|
||||||
root.dialectReady = true;
|
|
||||||
generateLayoutConfig();
|
generateLayoutConfig();
|
||||||
generateInputConfig();
|
generateInputConfig();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateOutputsConfig(outputsData, callback, skipReload) {
|
function generateOutputsConfig(outputsData, callback, skipReload) {
|
||||||
@@ -612,7 +599,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
// outputs.conf is sourced first, so its rules win over config.conf's; read both for carried fields.
|
// outputs.conf is sourced first, so its rules win over config.conf's; read both for carried fields.
|
||||||
Proc.runCommand("mango-read-outputs", ["sh", "-c", `cat "${outputsPath}" "${configPath}" 2>/dev/null`], existing => {
|
Proc.runCommand("mango-read-outputs", ["sh", "-c", `cat "${outputsPath}" "${configPath}" 2>/dev/null`], existing => {
|
||||||
const lines = ["# Auto-generated by DMS - do not edit manually", ""].concat(OutputModel.mangoMonitorRuleLines(outputsData, existing, root.snakeKeys), [""]);
|
const lines = ["# Auto-generated by DMS - do not edit manually", ""].concat(OutputModel.mangoMonitorRuleLines(outputsData, existing), [""]);
|
||||||
const content = lines.join("\n");
|
const content = lines.join("\n");
|
||||||
|
|
||||||
suppressWatchedConfigReloads(1500);
|
suppressWatchedConfigReloads(1500);
|
||||||
@@ -646,21 +633,16 @@ Singleton {
|
|||||||
const gapsOut = (gapsOverride >= 0 && SettingsData.mangoLayoutGapsOutOverride >= 0) ? SettingsData.mangoLayoutGapsOutOverride : gapsIn;
|
const gapsOut = (gapsOverride >= 0 && SettingsData.mangoLayoutGapsOutOverride >= 0) ? SettingsData.mangoLayoutGapsOutOverride : gapsIn;
|
||||||
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
|
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
|
||||||
|
|
||||||
const snake = root.snakeKeys;
|
|
||||||
let content = `# Auto-generated by DMS - do not edit manually
|
let content = `# Auto-generated by DMS - do not edit manually
|
||||||
border_radius=${cornerRadius}
|
border_radius=${cornerRadius}
|
||||||
${snake ? "border_px" : "borderpx"}=${borderSize}
|
border_px=${borderSize}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (manageGaps)
|
if (manageGaps)
|
||||||
content += snake ? `gap_inner_horizontal=${gapsIn}
|
content += `gap_inner_horizontal=${gapsIn}
|
||||||
gap_inner_vertical=${gapsIn}
|
gap_inner_vertical=${gapsIn}
|
||||||
gap_outer_horizontal=${gapsOut}
|
gap_outer_horizontal=${gapsOut}
|
||||||
gap_outer_vertical=${gapsOut}
|
gap_outer_vertical=${gapsOut}
|
||||||
` : `gappih=${gapsIn}
|
|
||||||
gappiv=${gapsIn}
|
|
||||||
gappoh=${gapsOut}
|
|
||||||
gappov=${gapsOut}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
suppressWatchedConfigReloads(1500);
|
suppressWatchedConfigReloads(1500);
|
||||||
@@ -727,14 +709,9 @@ cursor_size=${size}`;
|
|||||||
inputGenerationPending = false;
|
inputGenerationPending = false;
|
||||||
if (!CompositorService.isMango)
|
if (!CompositorService.isMango)
|
||||||
return;
|
return;
|
||||||
// numlock is spelled per dialect, so the first write waits for mango-migrate
|
|
||||||
if (!dialectReady) {
|
|
||||||
inputGenerationPending = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inputGenerationRunning = true;
|
inputGenerationRunning = true;
|
||||||
|
|
||||||
const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData, snakeKeys), [""]).join("\n");
|
const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData), [""]).join("\n");
|
||||||
// Exit 3 = unchanged, so a shell start does not reload Mango for nothing
|
// Exit 3 = unchanged, so a shell start does not reload Mango for nothing
|
||||||
const script = `mkdir -p "${mangoDmsDir}" && cat > "${inputPath}.tmp" << 'EOF'\n${content}EOF\nif cmp -s "${inputPath}.tmp" "${inputPath}"; then rm -f "${inputPath}.tmp"; exit 3; fi\nmv -f "${inputPath}.tmp" "${inputPath}"`;
|
const script = `mkdir -p "${mangoDmsDir}" && cat > "${inputPath}.tmp" << 'EOF'\n${content}EOF\nif cmp -s "${inputPath}.tmp" "${inputPath}"; then rm -f "${inputPath}.tmp"; exit 3; fi\nmv -f "${inputPath}.tmp" "${inputPath}"`;
|
||||||
suppressWatchedConfigReloads(1500);
|
suppressWatchedConfigReloads(1500);
|
||||||
@@ -757,7 +734,7 @@ cursor_size=${size}`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mouse and trackpad keys are the split names Mango has had since 0.16.1; libinput enums are numeric
|
// Mouse and trackpad keys are the split names Mango has had since 0.16.1; libinput enums are numeric
|
||||||
function mangoInputLines(s, snake) {
|
function mangoInputLines(s) {
|
||||||
const flag = value => value ? 1 : 0;
|
const flag = value => value ? 1 : 0;
|
||||||
const profile = name => name === "flat" ? 1 : name === "adaptive" ? 2 : -1;
|
const profile = name => name === "flat" ? 1 : name === "adaptive" ? 2 : -1;
|
||||||
const scrollMethod = name => ({
|
const scrollMethod = name => ({
|
||||||
@@ -806,7 +783,7 @@ cursor_size=${size}`;
|
|||||||
add("repeat_delay", s.keyboardRepeatDelay);
|
add("repeat_delay", s.keyboardRepeatDelay);
|
||||||
if (s.keyboardRepeatRate > 0)
|
if (s.keyboardRepeatRate > 0)
|
||||||
add("repeat_rate", s.keyboardRepeatRate);
|
add("repeat_rate", s.keyboardRepeatRate);
|
||||||
add(snake ? "numlock_on" : "numlockon", flag(s.keyboardNumlock));
|
add("numlock_on", flag(s.keyboardNumlock));
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ Singleton {
|
|||||||
property bool _mprisPublishDirty: false
|
property bool _mprisPublishDirty: false
|
||||||
property int _mprisConnectionEpoch: 0
|
property int _mprisConnectionEpoch: 0
|
||||||
property int positionConsumers: 0
|
property int positionConsumers: 0
|
||||||
|
property bool refreshingPosition: false
|
||||||
|
|
||||||
|
function refreshPosition(player) {
|
||||||
|
if (!player?.positionSupported)
|
||||||
|
return;
|
||||||
|
refreshingPosition = true;
|
||||||
|
player.positionChanged();
|
||||||
|
refreshingPosition = false;
|
||||||
|
}
|
||||||
|
|
||||||
function addPositionRef() {
|
function addPositionRef() {
|
||||||
positionConsumers++;
|
positionConsumers++;
|
||||||
@@ -398,7 +407,7 @@ Singleton {
|
|||||||
interval: 1000
|
interval: 1000
|
||||||
running: root.positionConsumers > 0 && root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
running: root.positionConsumers > 0 && root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: root.activePlayer?.positionChanged()
|
onTriggered: root.refreshPosition(root.activePlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _hasMPRISSubscription(): bool {
|
function _hasMPRISSubscription(): bool {
|
||||||
|
|||||||
@@ -334,9 +334,18 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sameScales(displayScales, scales))
|
||||||
|
return;
|
||||||
displayScales = scales;
|
displayScales = scales;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameScales(a, b) {
|
||||||
|
const names = Object.keys(a);
|
||||||
|
if (names.length !== Object.keys(b).length)
|
||||||
|
return false;
|
||||||
|
return names.every(name => a[name] === b[name]);
|
||||||
|
}
|
||||||
|
|
||||||
function sortWindowsByLayout(windowList) {
|
function sortWindowsByLayout(windowList) {
|
||||||
const enriched = windowList.map(w => {
|
const enriched = windowList.map(w => {
|
||||||
const ws = workspaces[w.workspace_id];
|
const ws = workspaces[w.workspace_id];
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory);
|
// A FolderListModel never given a folder lists the working directory.
|
||||||
|
userWatcher.folder = AdminPolicy.disableUserPlugins ? "" : Paths.toFileUrl(root.pluginDirectory);
|
||||||
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory);
|
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory);
|
||||||
Qt.callLater(resyncAll);
|
Qt.callLater(resyncAll);
|
||||||
Qt.callLater(checkPluginDirectoryExists);
|
Qt.callLater(checkPluginDirectoryExists);
|
||||||
@@ -150,7 +151,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resyncAll() {
|
function resyncAll() {
|
||||||
const userList = snapshotModel(userWatcher, "user");
|
const userList = AdminPolicy.disableUserPlugins ? [] : snapshotModel(userWatcher, "user");
|
||||||
const sysList = snapshotModel(systemWatcher, "system");
|
const sysList = snapshotModel(systemWatcher, "system");
|
||||||
const seenPaths = {};
|
const seenPaths = {};
|
||||||
const modifiedTimes = {};
|
const modifiedTimes = {};
|
||||||
@@ -308,8 +309,21 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _stripDotSlash(p) {
|
function _pluginFilePath(dir, rel) {
|
||||||
return p.startsWith("./") ? p.slice(2) : p;
|
if (!rel)
|
||||||
|
return "";
|
||||||
|
const path = String(rel);
|
||||||
|
let decoded;
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(path);
|
||||||
|
} catch (e) {
|
||||||
|
decoded = "..";
|
||||||
|
}
|
||||||
|
if (decoded.split("/").includes("..")) {
|
||||||
|
log.warn("plugin path outside its directory, refusing:", path, "in", dir);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return dir + "/" + (path.startsWith("./") ? path.slice(2) : path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _deriveLegacySurface(type, capabilities) {
|
function _deriveLegacySurface(type, capabilities) {
|
||||||
@@ -334,17 +348,16 @@ Singleton {
|
|||||||
log.warn("unknown plugin surface", surface, "in", dir);
|
log.warn("unknown plugin surface", surface, "in", dir);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rel = manifest.components[surface];
|
const path = _pluginFilePath(dir, manifest.components[surface]);
|
||||||
if (!rel)
|
if (!path)
|
||||||
continue;
|
continue;
|
||||||
paths[surface] = dir + "/" + _stripDotSlash(rel);
|
paths[surface] = path;
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
if (manifest.component) {
|
const legacyPath = _pluginFilePath(dir, manifest.component);
|
||||||
const surface = _deriveLegacySurface(manifest.type, manifest.capabilities);
|
if (legacyPath)
|
||||||
paths[surface] = dir + "/" + _stripDotSlash(manifest.component);
|
paths[_deriveLegacySurface(manifest.type, manifest.capabilities)] = legacyPath;
|
||||||
}
|
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,12 +373,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dir = absPath.substring(0, absPath.lastIndexOf('/'));
|
const dir = absPath.substring(0, absPath.lastIndexOf('/'));
|
||||||
let settings = manifest.settings;
|
const settingsPath = _pluginFilePath(dir, manifest.settings);
|
||||||
if (settings && settings.startsWith("./"))
|
const startupCheckPath = _pluginFilePath(dir, manifest.startupCheck);
|
||||||
settings = settings.slice(2);
|
|
||||||
let startupCheck = manifest.startupCheck;
|
|
||||||
if (startupCheck && startupCheck.startsWith("./"))
|
|
||||||
startupCheck = startupCheck.slice(2);
|
|
||||||
|
|
||||||
const componentPaths = _resolveComponentPaths(manifest, dir);
|
const componentPaths = _resolveComponentPaths(manifest, dir);
|
||||||
const surfaces = Object.keys(componentPaths);
|
const surfaces = Object.keys(componentPaths);
|
||||||
@@ -397,8 +406,8 @@ Singleton {
|
|||||||
info.componentPaths = componentPaths;
|
info.componentPaths = componentPaths;
|
||||||
info.surfaces = surfaces;
|
info.surfaces = surfaces;
|
||||||
info.componentPath = componentPaths.widget || componentPaths[surfaces[0]];
|
info.componentPath = componentPaths.widget || componentPaths[surfaces[0]];
|
||||||
info.settingsPath = settings ? (dir + "/" + settings) : null;
|
info.settingsPath = settingsPath || null;
|
||||||
info.startupCheckPath = startupCheck ? (dir + "/" + startupCheck) : null;
|
info.startupCheckPath = startupCheckPath || null;
|
||||||
info.loaded = isPluginLoaded(manifest.id);
|
info.loaded = isPluginLoaded(manifest.id);
|
||||||
info.type = manifest.type || (manifest.components ? "composite" : "widget");
|
info.type = manifest.type || (manifest.components ? "composite" : "widget");
|
||||||
info.source = sourceTag;
|
info.source = sourceTag;
|
||||||
@@ -1146,7 +1155,8 @@ Singleton {
|
|||||||
const userUrl = Paths.toFileUrl(root.pluginDirectory);
|
const userUrl = Paths.toFileUrl(root.pluginDirectory);
|
||||||
const systemUrl = Paths.toFileUrl(root.systemPluginDirectory);
|
const systemUrl = Paths.toFileUrl(root.systemPluginDirectory);
|
||||||
userWatcher.folder = "";
|
userWatcher.folder = "";
|
||||||
userWatcher.folder = userUrl;
|
if (!AdminPolicy.disableUserPlugins)
|
||||||
|
userWatcher.folder = userUrl;
|
||||||
systemWatcher.folder = "";
|
systemWatcher.folder = "";
|
||||||
systemWatcher.folder = systemUrl;
|
systemWatcher.folder = systemUrl;
|
||||||
resyncDebounce.restart();
|
resyncDebounce.restart();
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ Singleton {
|
|||||||
profileImage = "";
|
profileImage = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true") {
|
|
||||||
profileImage = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!freedeskAvailable) {
|
if (!freedeskAvailable) {
|
||||||
profileImage = "";
|
profileImage = "";
|
||||||
return;
|
return;
|
||||||
@@ -296,52 +291,6 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
property string pendingGreeterProfileUser: ""
|
|
||||||
|
|
||||||
function getGreeterUserProfileImage(username) {
|
|
||||||
if (!username) {
|
|
||||||
profileImage = "";
|
|
||||||
pendingGreeterProfileUser = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof GreeterUsersService !== "undefined") {
|
|
||||||
const cachedPath = GreeterUsersService.profileImagePath(username);
|
|
||||||
if (cachedPath) {
|
|
||||||
profileImage = cachedPath;
|
|
||||||
pendingGreeterProfileUser = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingGreeterProfileUser = username;
|
|
||||||
userProfileCheckProcess.command = ["bash", "-c", `uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`];
|
|
||||||
userProfileCheckProcess.running = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: userProfileCheckProcess
|
|
||||||
command: []
|
|
||||||
running: false
|
|
||||||
|
|
||||||
stdout: StdioCollector {
|
|
||||||
onStreamFinished: {
|
|
||||||
const trimmed = text.trim();
|
|
||||||
if (trimmed && trimmed !== "" && !trimmed.includes("Error") && trimmed !== "/var/lib/AccountsService/icons/") {
|
|
||||||
root.profileImage = trimmed;
|
|
||||||
} else {
|
|
||||||
root.profileImage = "";
|
|
||||||
}
|
|
||||||
root.pendingGreeterProfileUser = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode !== 0 && root.pendingGreeterProfileUser !== "") {
|
|
||||||
root.profileImage = "";
|
|
||||||
root.pendingGreeterProfileUser = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "profile"
|
target: "profile"
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Created late from the async DMSShell loader, after loaded and capabilitiesReceived may have fired.
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (SessionData._hasLoaded)
|
||||||
|
updateCyclingState();
|
||||||
|
}
|
||||||
|
|
||||||
function updateCyclingState() {
|
function updateCyclingState() {
|
||||||
cyclingActive = serverSchedulingAvailable && (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper);
|
cyclingActive = serverSchedulingAvailable && (SessionData.wallpaperCyclingEnabled || SessionData.perMonitorWallpaper);
|
||||||
if (!cyclingActive && !_lastPushedEnabled)
|
if (!cyclingActive && !_lastPushedEnabled)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user