mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-23 11:44:00 -04:00
Compare commits
41
Commits
master
...
stable-2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09449c0b37 | ||
|
|
6c3a646dc4 | ||
|
|
a680e1bd39 | ||
|
|
36ebe0e3ef | ||
|
|
0102bf6d40 | ||
|
|
dbf07ea644 | ||
|
|
58d741ec07 | ||
|
|
2befd45974 | ||
|
|
99eb2a06d0 | ||
|
|
163c4d5a1f | ||
|
|
d4ecaf82d4 | ||
|
|
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)
|
||||||
|
}
|
||||||
|
mangoParseConfig = func(string) (string, error) { return "", os.ErrNotExist }
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
if got := checkMangoConfig(dir, mangoconf.Snake); len(got) != 2 || got[0].message != "Not snake keys: config.conf" {
|
|
||||||
t.Fatalf("snake: %+v", got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
@@ -19,6 +20,23 @@ type File struct {
|
|||||||
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if policy != nil {
|
||||||
|
policies = append(policies, policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if policy != nil && policy.DisableRegistries != nil {
|
return flag(policies, func(p *File) *bool { return p.DisableRegistries }), nil
|
||||||
disabled = *policy.DisableRegistries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 disabled, nil
|
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,12 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
# layer_animations would tween every DMS layer move; override with no_animation:0 rules after the source lines.
|
||||||
|
layer_rule=no_animation:1,layer_name:^(quickshell)$
|
||||||
|
layer_rule=no_animation:1,layer_name:^dms:.*
|
||||||
|
|||||||
@@ -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,6 +30,14 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case dbusSessionInterface + ".Unlock":
|
case dbusSessionInterface + ".Unlock":
|
||||||
|
if m.keepLockOnUnlock {
|
||||||
|
// Only the locker clears LockedHint, so take both from logind, and
|
||||||
|
// keep the cached state when logind cannot be read.
|
||||||
|
if m.sessionObj != nil && m.updateSessionState() == nil && !m.snapshotState().Locked {
|
||||||
|
m.prelockedReady.Store(false)
|
||||||
|
}
|
||||||
|
m.notifySubscribers()
|
||||||
|
} else {
|
||||||
lockedHint := m.sessionLockedHint()
|
lockedHint := m.sessionLockedHint()
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Locked = false
|
m.state.Locked = false
|
||||||
@@ -37,6 +45,7 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
m.notifySubscribers()
|
m.notifySubscribers()
|
||||||
m.prelockedReady.Store(false)
|
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()
|
||||||
|
|||||||
@@ -38,19 +38,22 @@ 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,
|
||||||
@@ -58,21 +61,34 @@ func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
|
|||||||
},
|
},
|
||||||
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,13 +6,12 @@ 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)
|
||||||
@@ -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...fb4853fbf1
Generated
+3
-3
@@ -3,11 +3,11 @@
|
|||||||
"dank-qml-common": {
|
"dank-qml-common": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1792513666,
|
"lastModified": 1792767358,
|
||||||
"narHash": "sha256-RDqPdPrM5lnwRrfFGLz3IwgTjCV2Ie+nrTD3ldj1t/s=",
|
"narHash": "sha256-0YG9NpPzk6jo2Y4amMYlCbVdE5Mdkv/WD2suSJj18CE=",
|
||||||
"owner": "duxlabs-dev",
|
"owner": "duxlabs-dev",
|
||||||
"repo": "dux-qml-common",
|
"repo": "dux-qml-common",
|
||||||
"rev": "ebf5e6c002da1bdaa4aaa2f319fea7b8507abc42",
|
"rev": "fb4853fbf1e8a8a4ae8dc21d33daf20590188887",
|
||||||
"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) {
|
|
||||||
case "niri":
|
|
||||||
// niri floors logical sizes, rounding up by one leaves a dead seam at snapped edges (#2526)
|
|
||||||
return {
|
return {
|
||||||
"w": Math.floor(phys.w / scale),
|
"w": roundLogical(phys.w / scale, compositor),
|
||||||
"h": Math.floor(phys.h / scale)
|
"h": roundLogical(phys.h / scale, compositor)
|
||||||
};
|
};
|
||||||
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 = [];
|
||||||
|
|||||||
@@ -338,9 +338,102 @@ Singleton {
|
|||||||
_requestPopout(popout, tabIndex, triggerSource, true);
|
_requestPopout(popout, tabIndex, triggerSource, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _islandBarBand(host, screen) {
|
||||||
|
const vertical = host.edge === "left" || host.edge === "right";
|
||||||
|
if (host.embedded) {
|
||||||
|
const start = vertical ? host.hostOriginX : host.hostOriginY;
|
||||||
|
return {
|
||||||
|
"start": start,
|
||||||
|
"end": start + host.bandThickness
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (host.freeMode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const extent = vertical ? screen.width : screen.height;
|
||||||
|
const instances = ShellLayout.forScreen(screen)?.instances ?? [];
|
||||||
|
let start = Infinity;
|
||||||
|
let end = -Infinity;
|
||||||
|
for (const instance of instances) {
|
||||||
|
if (instance.edge !== host.edge || instance.barId === host.barId || (instance.kind !== "bar" && instance.kind !== "frame") || instance.rowThickness <= 0)
|
||||||
|
continue;
|
||||||
|
const config = SettingsData.getBarConfig(instance.barId);
|
||||||
|
if (!config || config.enabled === false || config.visible === false)
|
||||||
|
continue;
|
||||||
|
const near = instance.rowOffset ?? 0;
|
||||||
|
const far = near + instance.rowThickness;
|
||||||
|
const bandStart = host.edge === "top" || host.edge === "left" ? near : extent - far;
|
||||||
|
const bandEnd = host.edge === "top" || host.edge === "left" ? far : extent - near;
|
||||||
|
start = Math.min(start, bandStart);
|
||||||
|
end = Math.max(end, bandEnd);
|
||||||
|
}
|
||||||
|
return end > start ? {
|
||||||
|
"start": start,
|
||||||
|
"end": end
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
function getActiveSurfaces() {
|
function getActiveSurfaces() {
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
|
// The host registry covers embedded, standalone, and free Islands with their live visual bounds.
|
||||||
|
for (const key of Object.keys(IslandHostRegistry.hosts || {})) {
|
||||||
|
const host = IslandHostRegistry.hosts[key];
|
||||||
|
const screen = host?.screen;
|
||||||
|
const controller = host?.islandController;
|
||||||
|
const snapVisible = controller?.expanded || (!host?.embedded && host?.motionRunning);
|
||||||
|
if (!screen?.name || !snapVisible || host.surfaceOpacity <= 0)
|
||||||
|
continue;
|
||||||
|
const rect = host.surfaceBodyRect?.();
|
||||||
|
if (!rect || rect.width <= 0 || rect.height <= 0)
|
||||||
|
continue;
|
||||||
|
let x = rect.x;
|
||||||
|
let y = rect.y;
|
||||||
|
let width = rect.width;
|
||||||
|
let height = rect.height;
|
||||||
|
const barBand = _islandBarBand(host, screen);
|
||||||
|
if (barBand) {
|
||||||
|
switch (host.edge) {
|
||||||
|
case "top":
|
||||||
|
if (y < barBand.end && y + height > barBand.start) {
|
||||||
|
const inset = Math.max(0, Math.min(height, barBand.end - y));
|
||||||
|
y += inset;
|
||||||
|
height -= inset;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "bottom":
|
||||||
|
if (y + height > barBand.start && y < barBand.end)
|
||||||
|
height = Math.max(0, barBand.start - y);
|
||||||
|
break;
|
||||||
|
case "left":
|
||||||
|
if (x < barBand.end && x + width > barBand.start) {
|
||||||
|
const inset = Math.max(0, Math.min(width, barBand.end - x));
|
||||||
|
x += inset;
|
||||||
|
width -= inset;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
if (x + width > barBand.start && x < barBand.end)
|
||||||
|
width = Math.max(0, barBand.start - x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const left = Math.max(0, x);
|
||||||
|
const top = Math.max(0, y);
|
||||||
|
const right = Math.min(screen.width, x + width);
|
||||||
|
const bottom = Math.min(screen.height, y + height);
|
||||||
|
if (right <= left || bottom <= top)
|
||||||
|
continue;
|
||||||
|
results.push({
|
||||||
|
"name": "dms:dankisland-surface",
|
||||||
|
"type": "island",
|
||||||
|
"x": Math.round((screen.x ?? 0) + left),
|
||||||
|
"y": Math.round((screen.y ?? 0) + top),
|
||||||
|
"width": Math.round(right - left),
|
||||||
|
"height": Math.round(bottom - top)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const screenName in currentPopoutsByScreen) {
|
for (const screenName in currentPopoutsByScreen) {
|
||||||
const popout = currentPopoutsByScreen[screenName];
|
const popout = currentPopoutsByScreen[screenName];
|
||||||
if (!popout || !popout.shouldBeVisible)
|
if (!popout || !popout.shouldBeVisible)
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -1128,6 +1126,7 @@ Singleton {
|
|||||||
property string customPowerActionPowerOff: Spec.SPEC.customPowerActionPowerOff.def
|
property string customPowerActionPowerOff: Spec.SPEC.customPowerActionPowerOff.def
|
||||||
property var customPowerButtons: Spec.SPEC.customPowerButtons.def
|
property var customPowerButtons: Spec.SPEC.customPowerButtons.def
|
||||||
property var powerMenuBootEntries: Spec.SPEC.powerMenuBootEntries.def
|
property var powerMenuBootEntries: Spec.SPEC.powerMenuBootEntries.def
|
||||||
|
property bool powerMenuFirmwareSetup: Spec.SPEC.powerMenuFirmwareSetup.def
|
||||||
|
|
||||||
property bool updaterCheckOnStart: Spec.SPEC.updaterCheckOnStart.def
|
property bool updaterCheckOnStart: Spec.SPEC.updaterCheckOnStart.def
|
||||||
property bool updaterUseCustomCommand: Spec.SPEC.updaterUseCustomCommand.def
|
property bool updaterUseCustomCommand: Spec.SPEC.updaterUseCustomCommand.def
|
||||||
@@ -1435,8 +1434,6 @@ Singleton {
|
|||||||
signal notificationPopupsInvalidated
|
signal notificationPopupsInvalidated
|
||||||
|
|
||||||
function refreshAuthAvailability() {
|
function refreshAuthAvailability() {
|
||||||
if (isGreeterMode)
|
|
||||||
return;
|
|
||||||
Processes.detectAuthCapabilities();
|
Processes.detectAuthCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1506,8 +1503,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 +1659,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 +1666,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 +1680,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 +1829,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 +2052,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 +2079,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));
|
||||||
}
|
}
|
||||||
@@ -2402,7 +2389,7 @@ Singleton {
|
|||||||
let name = I18n.tr("Dock");
|
let name = I18n.tr("Dock");
|
||||||
let number = 2;
|
let number = 2;
|
||||||
while (dockConfigs.some(config => config.name === name))
|
while (dockConfigs.some(config => config.name === name))
|
||||||
name = I18n.tr("Dock") + " " + number++;
|
name = I18n.tr("Dock %1", "generated dock name, %1 is a number").arg(number++);
|
||||||
const config = DockConfig.create(id, name);
|
const config = DockConfig.create(id, name);
|
||||||
const edge = firstFreeDockEdge(id, config.screenPreferences);
|
const edge = firstFreeDockEdge(id, config.screenPreferences);
|
||||||
config.enabled = edge >= 0;
|
config.enabled = edge >= 0;
|
||||||
@@ -2859,6 +2846,9 @@ Singleton {
|
|||||||
id: "dot" + Date.now(),
|
id: "dot" + Date.now(),
|
||||||
name: I18n.tr("Dot", "bar layout: free-floating dot that opens island activities"),
|
name: I18n.tr("Dot", "bar layout: free-floating dot that opens island activities"),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
// A hidden or auto-hiding base bar would switch off the dot's OSD and notifications, and bar IPC refuses dot configs.
|
||||||
|
visible: true,
|
||||||
|
autoHide: false,
|
||||||
island: false,
|
island: false,
|
||||||
dot: true,
|
dot: true,
|
||||||
screenPreferences: ["all"],
|
screenPreferences: ["all"],
|
||||||
@@ -3725,11 +3715,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 +3730,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (isGreeterMode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
_loading = true;
|
_loading = true;
|
||||||
const hadParseFailed = hasParseFailed;
|
const hadParseFailed = hasParseFailed;
|
||||||
@@ -3776,9 +3763,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 +3906,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 +3947,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
id: settingsFolderLoader
|
id: settingsFolderLoader
|
||||||
active: !isGreeterMode
|
|
||||||
function unloadModel() {
|
function unloadModel() {
|
||||||
active = false;
|
active = false;
|
||||||
}
|
}
|
||||||
@@ -4012,59 +3995,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 +4008,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,
|
||||||
|
|||||||
+41
-49
@@ -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,16 +1415,13 @@ 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) {
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.matugenSmartMode) {
|
if (typeof SettingsData !== "undefined" && SettingsData.matugenSmartMode) {
|
||||||
@@ -1447,18 +1437,15 @@ 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) {
|
||||||
setLightMode(!isLightMode, savePrefs, true);
|
setLightMode(!isLightMode, 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;
|
||||||
@@ -2144,7 +2130,7 @@ Singleton {
|
|||||||
function applyGtkColors() {
|
function applyGtkColors() {
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors", "error toast, %1 is GTK or Qt").arg("GTK"));
|
ToastService.showError(I18n.tr("%1 not available or disabled - cannot apply %2 colors", "error toast, %1 is matugen, %2 is GTK or Qt").arg("matugen").arg("GTK"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2166,7 +2152,7 @@ Singleton {
|
|||||||
function applyQtColors() {
|
function applyQtColors() {
|
||||||
if (!matugenAvailable) {
|
if (!matugenAvailable) {
|
||||||
if (typeof ToastService !== "undefined") {
|
if (typeof ToastService !== "undefined") {
|
||||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors").arg("Qt"));
|
ToastService.showError(I18n.tr("%1 not available or disabled - cannot apply %2 colors", "error toast, %1 is matugen, %2 is GTK or Qt").arg("matugen").arg("Qt"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ var ISLAND_DEFAULTS = {
|
|||||||
islandPlacement: "edge",
|
islandPlacement: "edge",
|
||||||
islandFreeSize: 48,
|
islandFreeSize: 48,
|
||||||
islandFreeIcon: "blur_on",
|
islandFreeIcon: "blur_on",
|
||||||
|
islandFreeIconMode: "icon",
|
||||||
|
islandFreeIconPath: "",
|
||||||
|
islandFreeIconColor: "primary",
|
||||||
islandFreeEdgeMargin: 2,
|
islandFreeEdgeMargin: 2,
|
||||||
islandFreeIdleDelay: 2500,
|
islandFreeIdleDelay: 2500,
|
||||||
islandFreeIdleOpacity: 0.45,
|
islandFreeIdleOpacity: 0.45,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -533,7 +533,7 @@ Singleton {
|
|||||||
root.toastRequested(0, message, "", "", "auth-sync");
|
root.toastRequested(0, message, "", "", "auth-sync");
|
||||||
} else {
|
} else {
|
||||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
||||||
root.toastRequested(2, I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
|
root.toastRequested(2, I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run '%1' manually.", "terminal fallback status, %1 is a command").arg("dms auth sync") + " " + I18n.tr("(exit %1)", "process exit code").arg(exitCode), details, "", "auth-sync");
|
||||||
}
|
}
|
||||||
root.finishAuthApply();
|
root.finishAuthApply();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1445,6 +1445,9 @@ var LOCAL_SPEC = {
|
|||||||
powerMenuBootEntries: {
|
powerMenuBootEntries: {
|
||||||
def: []
|
def: []
|
||||||
},
|
},
|
||||||
|
powerMenuFirmwareSetup: {
|
||||||
|
def: false
|
||||||
|
},
|
||||||
updaterCheckOnStart: {
|
updaterCheckOnStart: {
|
||||||
def: false
|
def: 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";
|
||||||
|
|||||||
@@ -146,11 +146,10 @@ DModal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoDisplay entries are excluded from DesktopEntries.applications but
|
// NoDisplay handlers stay hidden like in the launcher, except the shell's own (notepad)
|
||||||
// remain valid mime handlers; resolve them by id so they stay pickable
|
|
||||||
for (const rawId of mimeMatchedRawIds) {
|
for (const rawId of mimeMatchedRawIds) {
|
||||||
const normId = _normAppId(rawId);
|
const normId = _normAppId(rawId);
|
||||||
if (normId === "dms-open" || listedIds.has(normId))
|
if (!normId.startsWith("dev.duxlabs.shell.") || listedIds.has(normId))
|
||||||
continue;
|
continue;
|
||||||
const desktopId = rawId.replace(/\.desktop$/, "");
|
const desktopId = rawId.replace(/\.desktop$/, "");
|
||||||
const entry = DesktopEntries.byId(desktopId) || DesktopEntries.heuristicLookup(desktopId);
|
const entry = DesktopEntries.byId(desktopId) || DesktopEntries.heuristicLookup(desktopId);
|
||||||
|
|||||||
@@ -123,18 +123,18 @@ DModal {
|
|||||||
text: {
|
text: {
|
||||||
switch (requestType) {
|
switch (requestType) {
|
||||||
case "confirm":
|
case "confirm":
|
||||||
return I18n.tr("Confirm passkey for ") + deviceName;
|
return I18n.tr("Confirm passkey for %1", "%1 is the device name").arg(deviceName);
|
||||||
case "display-passkey":
|
case "display-passkey":
|
||||||
return I18n.tr("Enter this passkey on ") + deviceName;
|
return I18n.tr("Enter this passkey on %1", "%1 is the device name").arg(deviceName);
|
||||||
case "authorize":
|
case "authorize":
|
||||||
return I18n.tr("Authorize pairing with ") + deviceName;
|
return I18n.tr("Authorize pairing with %1", "%1 is the device name").arg(deviceName);
|
||||||
case "pin":
|
case "pin":
|
||||||
return I18n.tr("Enter PIN for ") + deviceName;
|
return I18n.tr("Enter PIN for %1", "%1 is the device or network name").arg(deviceName);
|
||||||
case "passkey":
|
case "passkey":
|
||||||
return I18n.tr("Enter passkey for ") + deviceName;
|
return I18n.tr("Enter passkey for %1", "%1 is the device name").arg(deviceName);
|
||||||
default:
|
default:
|
||||||
if (requestType.startsWith("authorize-service"))
|
if (requestType.startsWith("authorize-service"))
|
||||||
return I18n.tr("Authorize service for ") + deviceName;
|
return I18n.tr("Authorize service for %1", "%1 is the device name").arg(deviceName);
|
||||||
return deviceName;
|
return deviceName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "More in " + ChangelogService.currentVersion
|
title: I18n.tr("More in %1", "changelog card title, %1 is a version").arg(ChangelogService.currentVersion)
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.additions
|
model: root.additions
|
||||||
@@ -258,7 +258,7 @@ Column {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: "Plugin authors"
|
text: I18n.tr("Plugin authors", "changelog section heading")
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Theme.fontWeightBold
|
font.weight: Theme.fontWeightBold
|
||||||
color: Theme.onTertiaryContainer
|
color: Theme.onTertiaryContainer
|
||||||
|
|||||||
@@ -634,7 +634,7 @@ Item {
|
|||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "files":
|
case "files":
|
||||||
if (!DSearchService.dsearchAvailable)
|
if (!DSearchService.dsearchAvailable)
|
||||||
return I18n.tr("File search requires dsearch\nInstall from github.com/duxlabs-dev/dux-search");
|
return I18n.tr("File search requires %1\nInstall from %2", "launcher file search empty state, %1 is the dsearch binary, %2 is its repository").arg("dsearch").arg("github.com/duxlabs-dev/dux-search");
|
||||||
if (!hasQuery)
|
if (!hasQuery)
|
||||||
return I18n.tr("Type to search files");
|
return I18n.tr("Type to search files");
|
||||||
if (root.controller.searchQuery.length < 2)
|
if (root.controller.searchQuery.length < 2)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ DFloatingWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("DMS_SOCKET not available")
|
text: I18n.tr("%1 not available", "%1 is a tool or environment variable name").arg("DMS_SOCKET")
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ DModal {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: I18n.tr("To sign in as a different user, log out and pick the account from the greeter. Creating a fresh session in parallel needs a multi-session greeter (greetd-flexiserver / GDM / LightDM).")
|
text: I18n.tr("To sign in as a different user, log out and pick the account from the greeter. Creating a fresh session in parallel needs a multi-session greeter (%1).", "switch user note, %1 lists multi-session greeters").arg("greetd-flexiserver / GDM / LightDM")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|||||||
@@ -315,16 +315,16 @@ DModal {
|
|||||||
}
|
}
|
||||||
supportingText: {
|
supportingText: {
|
||||||
if (promptReason === "pkcs11")
|
if (promptReason === "pkcs11")
|
||||||
return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
|
return I18n.tr("Enter PIN for %1", "%1 is the device or network name").arg(wifiPasswordSSID);
|
||||||
if (isCertificatePrompt)
|
if (isCertificatePrompt)
|
||||||
return wifiPasswordSSID;
|
return wifiPasswordSSID;
|
||||||
if (fieldsInfo.length > 0)
|
if (fieldsInfo.length > 0)
|
||||||
return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
|
return I18n.tr("Enter credentials for %1", "%1 is the network name").arg(wifiPasswordSSID);
|
||||||
if (isVpnPrompt)
|
if (isVpnPrompt)
|
||||||
return I18n.tr("Enter password for ") + wifiPasswordSSID;
|
return I18n.tr("Enter password for %1", "%1 is the network name").arg(wifiPasswordSSID);
|
||||||
if (isHiddenNetwork)
|
if (isHiddenNetwork)
|
||||||
return I18n.tr("Enter network name and password");
|
return I18n.tr("Enter network name and password");
|
||||||
return (requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ")) + wifiPasswordSSID;
|
return (requiresEnterprise ? I18n.tr("Enter credentials for %1", "%1 is the network name") : I18n.tr("Enter password for %1", "%1 is the network name")).arg(wifiPasswordSSID);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ DModal {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("WiFi QR code for ") + theItem.wifiSSID
|
text: I18n.tr("WiFi QR code for %1", "%1 is the network name").arg(theItem.wifiSSID)
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Theme.fontWeightMedium
|
font.weight: Theme.fontWeightMedium
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Item {
|
|||||||
visible: !DgopService.dgopAvailable || root.mounts.length === 0
|
visible: !DgopService.dgopAvailable || root.mounts.length === 0
|
||||||
iconName: DgopService.dgopAvailable ? "storage" : "error"
|
iconName: DgopService.dgopAvailable ? "storage" : "error"
|
||||||
iconColor: DgopService.dgopAvailable ? Theme.primary : Theme.error
|
iconColor: DgopService.dgopAvailable ? Theme.primary : Theme.error
|
||||||
title: DgopService.dgopAvailable ? I18n.tr("No disk data available") : I18n.tr("dgop not available")
|
title: DgopService.dgopAvailable ? I18n.tr("No disk data available") : I18n.tr("%1 not available", "%1 is a tool or environment variable name").arg("dgop")
|
||||||
}
|
}
|
||||||
|
|
||||||
CcGroup {
|
CcGroup {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ Item {
|
|||||||
visible: !root.hasProfiles
|
visible: !root.hasProfiles
|
||||||
iconName: "vpn_key_off"
|
iconName: "vpn_key_off"
|
||||||
title: I18n.tr("No VPN profiles")
|
title: I18n.tr("No VPN profiles")
|
||||||
subtitle: I18n.tr("Click Import to add a .ovpn or .conf")
|
subtitle: I18n.tr("Click Import to add a %1 or %2 file", "VPN import hint, %1 and %2 are file extensions").arg(".ovpn").arg(".conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
DListView {
|
DListView {
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ QtObject {
|
|||||||
"type": "action",
|
"type": "action",
|
||||||
"category": "system",
|
"category": "system",
|
||||||
"enabled": DgopService.dgopAvailable,
|
"enabled": DgopService.dgopAvailable,
|
||||||
"warning": !DgopService.dgopAvailable ? I18n.tr("Requires 'dgop' tool") : undefined,
|
"warning": !DgopService.dgopAvailable ? I18n.tr("Requires %1", "missing program").arg("dgop") : undefined,
|
||||||
"allowMultiple": true
|
"allowMultiple": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ Item {
|
|||||||
property real columns: 4
|
property real columns: 4
|
||||||
property real rows: 1
|
property real rows: 1
|
||||||
property bool compact: columns <= 2 && rows === 1
|
property bool compact: columns <= 2 && rows === 1
|
||||||
|
property bool small: false
|
||||||
|
readonly property real bodyInset: small ? Math.max(0, (height - CcMetrics.smallRowHeight) / 2) : 0
|
||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property string iconLabel: ""
|
property string iconLabel: ""
|
||||||
property string iconTooltip: ""
|
property string iconTooltip: ""
|
||||||
@@ -135,13 +137,13 @@ Item {
|
|||||||
onSliderValueChanged: newValue => root.sliderValueChanged(newValue)
|
onSliderValueChanged: newValue => root.sliderValueChanged(newValue)
|
||||||
|
|
||||||
Binding on trackHeight {
|
Binding on trackHeight {
|
||||||
when: !root.tall
|
when: !root.tall && !root.small
|
||||||
value: CcMetrics.stripTrackHeight
|
value: CcMetrics.stripTrackHeight
|
||||||
restoreMode: Binding.RestoreBinding
|
restoreMode: Binding.RestoreBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding on handleHeight {
|
Binding on handleHeight {
|
||||||
when: !root.tall
|
when: !root.tall && !root.small
|
||||||
value: CcMetrics.stripHandleHeight
|
value: CcMetrics.stripHandleHeight
|
||||||
restoreMode: Binding.RestoreBinding
|
restoreMode: Binding.RestoreBinding
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ CcTile {
|
|||||||
if (enabled)
|
if (enabled)
|
||||||
click();
|
click();
|
||||||
}
|
}
|
||||||
Accessible.name: I18n.tr("Copy") + " " + modelData
|
Accessible.name: I18n.tr("Copy %1", "%1 is a color value").arg(modelData)
|
||||||
onClicked: Quickshell.clipboardText = modelData
|
onClicked: Quickshell.clipboardText = modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ CcTile {
|
|||||||
}
|
}
|
||||||
subtitle: {
|
subtitle: {
|
||||||
if (!DgopService.dgopAvailable)
|
if (!DgopService.dgopAvailable)
|
||||||
return I18n.tr("DMS_SOCKET not available");
|
return I18n.tr("%1 not available", "%1 is a tool or environment variable name").arg("DMS_SOCKET");
|
||||||
if (!selectedMount)
|
if (!selectedMount)
|
||||||
return I18n.tr("No disk data available");
|
return I18n.tr("No disk data available");
|
||||||
return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`;
|
return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function isSliderWidget(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canShrink(id) {
|
function canShrink(id) {
|
||||||
return id !== "user" && !isSliderWidget(id);
|
return id !== "user";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSmall(widget, rows) {
|
function isSmall(widget, rows) {
|
||||||
@@ -25,7 +25,7 @@ function isSmall(widget, rows) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canGroup(widget) {
|
function canGroup(widget) {
|
||||||
return canShrink(widget?.id) && widget?.id !== GROUP_ID;
|
return canShrink(widget?.id) && !isSliderWidget(widget?.id) && widget?.id !== GROUP_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Members of a group that no longer exists fall back to the grid.
|
// Members of a group that no longer exists fall back to the grid.
|
||||||
@@ -38,10 +38,12 @@ function inGroup(widget, ids) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupMembers(widgets, groupId) {
|
function groupMembers(widgets, groupId) {
|
||||||
return widgets.reduce((members, widget, index) => widget?.group === groupId && isShown(widget) ? members.concat([{
|
return widgets.reduce((members, widget, index) => widget?.group === groupId && isShown(widget) ? members.concat([
|
||||||
|
{
|
||||||
"index": index,
|
"index": index,
|
||||||
"widget": widget
|
"widget": widget
|
||||||
}]) : members, []);
|
}
|
||||||
|
]) : members, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemovable(widget) {
|
function isRemovable(widget) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Card {
|
|||||||
readonly property string levelText: Math.round(BatteryService.batteryLevel) + "%"
|
readonly property string levelText: Math.round(BatteryService.batteryLevel) + "%"
|
||||||
readonly property string meterStyle: options.style ?? "solid"
|
readonly property string meterStyle: options.style ?? "solid"
|
||||||
readonly property bool levelColors: options.color !== "theme"
|
readonly property bool levelColors: options.color !== "theme"
|
||||||
readonly property string healthText: options.health === true && BatteryService.batteryHealth.endsWith("%") ? I18n.tr("Health") + " " + BatteryService.batteryHealth : ""
|
readonly property string healthText: options.health === true && BatteryService.batteryHealth.endsWith("%") ? I18n.tr("Health %1", "battery health, %1 is a percentage").arg(BatteryService.batteryHealth) : ""
|
||||||
|
|
||||||
entryId: "battery"
|
entryId: "battery"
|
||||||
tone: options.tone ?? ""
|
tone: options.tone ?? ""
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -3,20 +3,52 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.DCommon.Widgets
|
import qs.DCommon.Widgets
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var controller
|
required property var controller
|
||||||
property string iconName: "blur_on"
|
property string iconName: "blur_on"
|
||||||
|
property string iconMode: "icon"
|
||||||
|
property string iconPath: ""
|
||||||
|
property string iconColor: "primary"
|
||||||
|
|
||||||
readonly property real diameter: Math.min(width, height)
|
readonly property real diameter: Math.min(width, height)
|
||||||
|
readonly property real faceSize: Math.max(Theme.fontSizeSmall, Math.round(root.diameter * 0.46))
|
||||||
|
readonly property color symbolColor: {
|
||||||
|
switch (root.iconColor) {
|
||||||
|
case "":
|
||||||
|
case "primary":
|
||||||
|
return Theme.primary;
|
||||||
|
case "surface":
|
||||||
|
return Theme.surfaceText;
|
||||||
|
default:
|
||||||
|
return root.iconColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: logoLoader
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
active: root.iconMode !== "icon"
|
||||||
|
|
||||||
|
sourceComponent: LauncherLogo {
|
||||||
|
mode: root.iconMode
|
||||||
|
size: root.faceSize
|
||||||
|
colorOverride: root.iconColor
|
||||||
|
customPath: root.iconPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A logo mode with nothing to show (unknown compositor, empty custom path) falls back to the symbol.
|
||||||
DIcon {
|
DIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
visible: !(logoLoader.item as LauncherLogo)?.resolvedMode
|
||||||
name: root.iconName
|
name: root.iconName
|
||||||
size: Math.max(Theme.fontSizeSmall, Math.round(root.diameter * 0.46))
|
size: root.faceSize
|
||||||
color: Theme.primary
|
color: root.symbolColor
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ Item {
|
|||||||
property string requestedWindow: ""
|
property string requestedWindow: ""
|
||||||
property bool freeMode: false
|
property bool freeMode: false
|
||||||
property bool anchorSnaps: false
|
property bool anchorSnaps: false
|
||||||
property real anchorX: 0
|
property point anchor
|
||||||
property real anchorY: 0
|
// Velocity the next applyTarget seeds the spring with; a released drag flings from it.
|
||||||
|
property var flingSeed: null
|
||||||
property real freeMargin: 8
|
property real freeMargin: 8
|
||||||
// Measured from the band centre, not the band start.
|
// Measured from the band centre, not the band start.
|
||||||
property real anchorAlong: 0
|
property real anchorAlong: 0
|
||||||
@@ -220,8 +221,8 @@ Item {
|
|||||||
}
|
}
|
||||||
if (!root.freeMode)
|
if (!root.freeMode)
|
||||||
return target;
|
return target;
|
||||||
const x = root.clampFree(root.anchorX - target.width / 2, target.width, root.width);
|
const x = root.clampFree(root.anchor.x - target.width / 2, target.width, root.width);
|
||||||
const y = root.clampFree(root.anchorY - target.height / 2, target.height, root.height);
|
const y = root.clampFree(root.anchor.y - target.height / 2, target.height, root.height);
|
||||||
return Object.assign({}, target, {
|
return Object.assign({}, target, {
|
||||||
"offsetAlong": root.isVertical ? y + target.height / 2 - root.height / 2 : x + target.width / 2 - root.width / 2,
|
"offsetAlong": root.isVertical ? y + target.height / 2 - root.height / 2 : x + target.width / 2 - root.width / 2,
|
||||||
"offsetCross": root.isVertical ? x : y
|
"offsetCross": root.isVertical ? x : y
|
||||||
@@ -238,7 +239,9 @@ Item {
|
|||||||
fadeCompactCross = root.descriptorCross(controller.compactTarget);
|
fadeCompactCross = root.descriptorCross(controller.compactTarget);
|
||||||
if (motion.running)
|
if (motion.running)
|
||||||
root.unionMotionStartBounds();
|
root.unionMotionStartBounds();
|
||||||
motion.setTarget(root.resolveTarget(controller.targetDescriptor), seedVelocity);
|
const seed = seedVelocity ?? root.flingSeed;
|
||||||
|
root.flingSeed = null;
|
||||||
|
motion.setTarget(root.resolveTarget(controller.targetDescriptor), seed);
|
||||||
if (!motion.running)
|
if (!motion.running)
|
||||||
root.controller.releaseIdleVisuals();
|
root.controller.releaseIdleVisuals();
|
||||||
}
|
}
|
||||||
@@ -261,8 +264,7 @@ Item {
|
|||||||
root.applyTarget();
|
root.applyTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
onAnchorXChanged: root.syncAnchor()
|
onAnchorChanged: root.syncAnchor()
|
||||||
onAnchorYChanged: root.syncAnchor()
|
|
||||||
onAnchorAlongChanged: root.syncAnchor()
|
onAnchorAlongChanged: root.syncAnchor()
|
||||||
onAlongExtentChanged: root.syncAnchor()
|
onAlongExtentChanged: root.syncAnchor()
|
||||||
onBandAlongStartChanged: root.syncAnchor()
|
onBandAlongStartChanged: root.syncAnchor()
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ Item {
|
|||||||
property var centerSectionRect: null
|
property var centerSectionRect: null
|
||||||
property var trailingSectionRect: null
|
property var trailingSectionRect: null
|
||||||
readonly property int hostLayer: root.hostWindow?.dBarLayer ?? WlrLayer.Top
|
readonly property int hostLayer: root.hostWindow?.dBarLayer ?? WlrLayer.Top
|
||||||
property real anchorX: 0
|
property point anchor
|
||||||
property real anchorY: 0
|
|
||||||
property bool anchorSnaps: false
|
property bool anchorSnaps: false
|
||||||
property real freeScale: 1
|
property real freeScale: 1
|
||||||
property real freeOpacity: 1
|
property real freeOpacity: 1
|
||||||
@@ -60,6 +59,7 @@ Item {
|
|||||||
readonly property real currentVisualY: surface.currentVisualY
|
readonly property real currentVisualY: surface.currentVisualY
|
||||||
readonly property real currentVisualWidth: surface.currentVisualWidth
|
readonly property real currentVisualWidth: surface.currentVisualWidth
|
||||||
readonly property real currentVisualHeight: surface.currentVisualHeight
|
readonly property real currentVisualHeight: surface.currentVisualHeight
|
||||||
|
readonly property real currentSurfaceRadius: surface.currentSurfaceRadius
|
||||||
readonly property color surfaceColor: surface.surfaceColor
|
readonly property color surfaceColor: surface.surfaceColor
|
||||||
readonly property real surfaceOpacity: surface.surfaceOpacity
|
readonly property real surfaceOpacity: surface.surfaceOpacity
|
||||||
readonly property bool inputSuspended: controller.inputSuspended
|
readonly property bool inputSuspended: controller.inputSuspended
|
||||||
@@ -68,7 +68,9 @@ Item {
|
|||||||
// Held from expand until the collapse spring settles, so compact-to-compact morphs never resize the bar window.
|
// Held from expand until the collapse spring settles, so compact-to-compact morphs never resize the bar window.
|
||||||
property bool sheetOut: false
|
property bool sheetOut: false
|
||||||
readonly property bool surfaceFits: (root.isVertical ? root.windowWidth : root.windowHeight) >= root.hostThickness - 1
|
readonly property bool surfaceFits: (root.isVertical ? root.windowWidth : root.windowHeight) >= root.hostThickness - 1
|
||||||
readonly property bool hostReady: !root.embedded || root.frameHosted || root.surfaceFits
|
// A free host lifts to the whole output for the sheet; the expand waits for that size so no frame of the morph is clipped.
|
||||||
|
property bool hostLifted: true
|
||||||
|
readonly property bool hostReady: root.freeMode ? root.hostLifted : (!root.embedded || root.frameHosted || root.surfaceFits)
|
||||||
// Layer surfaces glitch when they resize under a moving sheet: edit room waits for a frame at the grown size, and the grown size outlives the shrink spring.
|
// Layer surfaces glitch when they resize under a moving sheet: edit room waits for a frame at the grown size, and the grown size outlives the shrink spring.
|
||||||
readonly property bool surfaceResizes: !root.freeMode && !root.frameHosted
|
readonly property bool surfaceResizes: !root.freeMode && !root.frameHosted
|
||||||
readonly property int editSurfaceHeight: controller.editSurfaceHeight
|
readonly property int editSurfaceHeight: controller.editSurfaceHeight
|
||||||
@@ -433,7 +435,8 @@ Item {
|
|||||||
homeBrightnessDisplay: SettingsData.islandLevelDisplay(root.barConfig, "islandHomeBrightnessDisplay")
|
homeBrightnessDisplay: SettingsData.islandLevelDisplay(root.barConfig, "islandHomeBrightnessDisplay")
|
||||||
batteryStyle: root.setting("islandBatteryStyle")
|
batteryStyle: root.setting("islandBatteryStyle")
|
||||||
mediaClockVisible: root.setting("islandMediaClockVisible")
|
mediaClockVisible: root.setting("islandMediaClockVisible")
|
||||||
mediaAutoSwitch: root.setting("islandMediaAutoSwitch")
|
// Home and media share the dot face; see mediaFaceToggleAvailable.
|
||||||
|
mediaAutoSwitch: !root.dotMode && root.setting("islandMediaAutoSwitch")
|
||||||
launcherCycleEnabled: SettingsData.launcherStyle === "island"
|
launcherCycleEnabled: SettingsData.launcherStyle === "island"
|
||||||
dashboardAvailableWidth: Math.max(0, (root.screen?.width ?? root.referenceScreenWidth) - (root.isVertical ? root.crossInsetStart + root.crossInsetEnd + root.outerGap + root.nearInset : root.alongInset) - Theme.spacingL * 2)
|
dashboardAvailableWidth: Math.max(0, (root.screen?.width ?? root.referenceScreenWidth) - (root.isVertical ? root.crossInsetStart + root.crossInsetEnd + root.outerGap + root.nearInset : root.alongInset) - Theme.spacingL * 2)
|
||||||
dashboardAvailableHeight: Math.max(0, (root.screen?.height ?? root.referenceScreenHeight) - (root.isVertical ? root.alongInset : root.crossInsetStart + root.crossInsetEnd + root.outerGap + root.nearInset) - Theme.spacingL * 2)
|
dashboardAvailableHeight: Math.max(0, (root.screen?.height ?? root.referenceScreenHeight) - (root.isVertical ? root.alongInset : root.crossInsetStart + root.crossInsetEnd + root.outerGap + root.nearInset) - Theme.spacingL * 2)
|
||||||
@@ -493,8 +496,7 @@ Item {
|
|||||||
hostOriginX: root.hostOriginX
|
hostOriginX: root.hostOriginX
|
||||||
hostOriginY: root.hostOriginY
|
hostOriginY: root.hostOriginY
|
||||||
freeMode: root.freeMode
|
freeMode: root.freeMode
|
||||||
anchorX: root.anchorX
|
anchor: root.anchor
|
||||||
anchorY: root.anchorY
|
|
||||||
anchorSnaps: root.anchorSnaps
|
anchorSnaps: root.anchorSnaps
|
||||||
anchorAlong: root.anchorAlong
|
anchorAlong: root.anchorAlong
|
||||||
nearInset: root.nearInset
|
nearInset: root.nearInset
|
||||||
@@ -534,6 +536,9 @@ Item {
|
|||||||
DotCompact {
|
DotCompact {
|
||||||
controller: controller
|
controller: controller
|
||||||
iconName: root.setting("islandFreeIcon")
|
iconName: root.setting("islandFreeIcon")
|
||||||
|
iconMode: root.setting("islandFreeIconMode")
|
||||||
|
iconPath: root.setting("islandFreeIconPath")
|
||||||
|
iconColor: root.setting("islandFreeIconColor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,10 +189,11 @@ Item {
|
|||||||
component CompactFace: Loader {
|
component CompactFace: Loader {
|
||||||
required property string activity
|
required property string activity
|
||||||
required property Component face
|
required property Component face
|
||||||
readonly property var target: root.resolveTarget(root.controller.compactTargetFor(activity))
|
// A free face only centres in the island, so it skips the anchor-resolved slot every drag event would rebuild.
|
||||||
|
readonly property var target: root.freeMode ? root.controller.compactTargetFor(activity) : root.resolveTarget(root.controller.compactTargetFor(activity))
|
||||||
readonly property bool isVertical: root.controller.isVertical
|
readonly property bool isVertical: root.controller.isVertical
|
||||||
readonly property real alongPos: !active ? 0 : Math.round(root.slotAlong(target)) - Math.round(isVertical ? root.islandY : root.islandX)
|
readonly property real alongPos: !active || root.freeMode ? 0 : Math.round(root.slotAlong(target)) - Math.round(isVertical ? root.islandY : root.islandX)
|
||||||
readonly property real crossPos: !active ? 0 : Math.round(root.slotCross(target)) - Math.round(isVertical ? root.islandX : root.islandY)
|
readonly property real crossPos: !active || root.freeMode ? 0 : Math.round(root.slotCross(target)) - Math.round(isVertical ? root.islandX : root.islandY)
|
||||||
|
|
||||||
x: root.freeMode ? Math.round((parent.width - width) / 2) : (isVertical ? crossPos : alongPos)
|
x: root.freeMode ? Math.round((parent.width - width) / 2) : (isVertical ? crossPos : alongPos)
|
||||||
y: root.freeMode ? Math.round((parent.height - height) / 2) : (isVertical ? alongPos : crossPos)
|
y: root.freeMode ? Math.round((parent.height - height) / 2) : (isVertical ? alongPos : crossPos)
|
||||||
@@ -209,11 +210,13 @@ Item {
|
|||||||
component ExpandedFace: Loader {
|
component ExpandedFace: Loader {
|
||||||
required property string activity
|
required property string activity
|
||||||
readonly property var target: root.controller.expandedTargetFor(activity)
|
readonly property var target: root.controller.expandedTargetFor(activity)
|
||||||
readonly property var slot: root.resolveTarget(target)
|
// A hidden face stays put, so a kept-alive sheet is not re-laid out on every move of the pill.
|
||||||
|
readonly property bool placed: active && visible
|
||||||
|
readonly property var slot: placed ? root.resolveTarget(target) : target
|
||||||
readonly property bool isVertical: root.controller.isVertical
|
readonly property bool isVertical: root.controller.isVertical
|
||||||
readonly property real inset: root.controller.farEdge ? 0 : root.expandedInset
|
readonly property real inset: root.controller.farEdge ? 0 : root.expandedInset
|
||||||
readonly property real alongPos: !active ? 0 : Math.round(root.slotAlong(slot)) - Math.round(isVertical ? root.islandY : root.islandX)
|
readonly property real alongPos: !placed ? 0 : Math.round(root.slotAlong(slot)) - Math.round(isVertical ? root.islandY : root.islandX)
|
||||||
readonly property real crossPos: !active ? 0 : Math.round(root.slotCross(slot)) - Math.round(isVertical ? root.islandX : root.islandY) + inset
|
readonly property real crossPos: !placed ? 0 : Math.round(root.slotCross(slot)) - Math.round(isVertical ? root.islandX : root.islandY) + inset
|
||||||
|
|
||||||
x: isVertical ? crossPos : alongPos
|
x: isVertical ? crossPos : alongPos
|
||||||
y: isVertical ? alongPos : crossPos
|
y: isVertical ? alongPos : crossPos
|
||||||
|
|||||||
@@ -618,7 +618,8 @@ QtObject {
|
|||||||
return mediaAvailable && mediaPreferred ? "media" : "home";
|
return mediaAvailable && mediaPreferred ? "media" : "home";
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool mediaFaceToggleAvailable: !expanded && mediaAvailable && (activeActivity === "home" || activeActivity === "media")
|
// A dot draws the same face for home and media, so a toggle would only change what the next click opens.
|
||||||
|
readonly property bool mediaFaceToggleAvailable: !dotMode && !expanded && mediaAvailable && (activeActivity === "home" || activeActivity === "media")
|
||||||
|
|
||||||
function toggleMediaFace() {
|
function toggleMediaFace() {
|
||||||
if (!mediaFaceToggleAvailable)
|
if (!mediaFaceToggleAvailable)
|
||||||
|
|||||||
@@ -10,52 +10,126 @@ DragHandler {
|
|||||||
|
|
||||||
readonly property real maxFlingSpeed: 4000
|
readonly property real maxFlingSpeed: 4000
|
||||||
readonly property real flingProjection: 0.09
|
readonly property real flingProjection: 0.09
|
||||||
property real startAnchorX: 0
|
// Single events are too noisy on high-rate mice, so the fling reads the stroke's last stretch.
|
||||||
property real startAnchorY: 0
|
readonly property int velocityWindow: 60
|
||||||
|
property point startAnchor
|
||||||
property point startPointer
|
property point startPointer
|
||||||
|
property point startOrigin
|
||||||
|
property point lastPointer
|
||||||
property bool tracking: false
|
property bool tracking: false
|
||||||
property real velocityX: 0
|
property bool movePending: false
|
||||||
property real velocityY: 0
|
property bool flingPending: false
|
||||||
|
property point releaseVelocity
|
||||||
|
property var samples: []
|
||||||
|
readonly property bool hostLifted: root.hostWindow.lifted
|
||||||
|
|
||||||
|
onHostLiftedChanged: {
|
||||||
|
if (root.hostLifted && root.flingPending)
|
||||||
|
root.fling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene positions are surface-local; events just after the lift can still carry the pre-lift origin, so the reading that continues the stroke wins.
|
||||||
function pointerPosition() {
|
function pointerPosition() {
|
||||||
return root.surface.mapToGlobal(root.centroid.position.x, root.centroid.position.y);
|
const p = root.centroid.scenePosition;
|
||||||
|
const current = Qt.point(p.x + root.hostWindow.frameLeft, p.y + root.hostWindow.frameTop);
|
||||||
|
if (!root.tracking)
|
||||||
|
return current;
|
||||||
|
const previous = Qt.point(p.x + root.startOrigin.x, p.y + root.startOrigin.y);
|
||||||
|
const last = root.lastPointer;
|
||||||
|
return Math.hypot(current.x - last.x, current.y - last.y) <= Math.hypot(previous.x - last.x, previous.y - last.y) ? current : previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
function anchorFor(pointer) {
|
||||||
|
return Qt.point(root.startAnchor.x + pointer.x - root.startPointer.x, root.startAnchor.y + pointer.y - root.startPointer.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberPress() {
|
function rememberPress() {
|
||||||
root.startAnchorX = root.surface.currentVisualX + root.surface.currentVisualWidth / 2;
|
root.startAnchor = Qt.point(root.surface.currentVisualX + root.surface.currentVisualWidth / 2, root.surface.currentVisualY + root.surface.currentVisualHeight / 2);
|
||||||
root.startAnchorY = root.surface.currentVisualY + root.surface.currentVisualHeight / 2;
|
root.startOrigin = Qt.point(root.hostWindow.frameLeft, root.hostWindow.frameTop);
|
||||||
root.startPointer = root.pointerPosition();
|
root.startPointer = root.pointerPosition();
|
||||||
root.velocityX = 0;
|
root.lastPointer = root.startPointer;
|
||||||
root.velocityY = 0;
|
root.samples = [];
|
||||||
root.tracking = true;
|
root.tracking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordPointer() {
|
||||||
|
const now = Date.now();
|
||||||
|
root.lastPointer = root.pointerPosition();
|
||||||
|
while (root.samples.length > 1 && now - root.samples[0].time > root.velocityWindow)
|
||||||
|
root.samples.shift();
|
||||||
|
root.samples.push({
|
||||||
|
"time": now,
|
||||||
|
"x": root.lastPointer.x,
|
||||||
|
"y": root.lastPointer.y
|
||||||
|
});
|
||||||
|
root.movePending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flingVelocity() {
|
||||||
|
const now = Date.now();
|
||||||
|
const recent = root.samples.filter(sample => now - sample.time <= root.velocityWindow);
|
||||||
|
if (recent.length < 2)
|
||||||
|
return Qt.point(0, 0);
|
||||||
|
const first = recent[0];
|
||||||
|
const last = recent[recent.length - 1];
|
||||||
|
const seconds = (last.time - first.time) / 1000;
|
||||||
|
if (seconds < 0.008)
|
||||||
|
return Qt.point(0, 0);
|
||||||
|
const clamp = value => Math.max(-root.maxFlingSpeed, Math.min(root.maxFlingSpeed, value));
|
||||||
|
return Qt.point(clamp((last.x - first.x) / seconds), clamp((last.y - first.y) / seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-based at drag start: a press just after a settle can read the old origin, and a flying dot has moved since the press.
|
||||||
function beginDrag() {
|
function beginDrag() {
|
||||||
if (!root.tracking)
|
root.flingPending = false;
|
||||||
|
root.tracking = false;
|
||||||
root.rememberPress();
|
root.rememberPress();
|
||||||
root.hostWindow.dragging = true;
|
root.hostWindow.dragging = true;
|
||||||
root.hostWindow.wake();
|
root.hostWindow.wake();
|
||||||
root.updateDrag();
|
root.recordPointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrag() {
|
function applyMove() {
|
||||||
const point = root.pointerPosition();
|
root.movePending = false;
|
||||||
root.velocityX = Math.max(-root.maxFlingSpeed, Math.min(root.maxFlingSpeed, root.centroid.velocity.x));
|
root.hostWindow.anchor = root.anchorFor(root.lastPointer);
|
||||||
root.velocityY = Math.max(-root.maxFlingSpeed, Math.min(root.maxFlingSpeed, root.centroid.velocity.y));
|
|
||||||
root.hostWindow.anchorX = root.startAnchorX + point.x - root.startPointer.x;
|
|
||||||
root.hostWindow.anchorY = root.startAnchorY + point.y - root.startPointer.y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishDrag() {
|
function finishDrag() {
|
||||||
root.hostWindow.dragging = false;
|
const host = root.hostWindow;
|
||||||
root.hostWindow.anchorX = root.hostWindow.clampAnchorX(root.hostWindow.anchorX + root.velocityX * root.flingProjection);
|
root.releaseVelocity = root.flingVelocity();
|
||||||
root.hostWindow.anchorY = root.hostWindow.clampAnchorY(root.hostWindow.anchorY + root.velocityY * root.flingProjection);
|
root.movePending = false;
|
||||||
root.surface.applyTarget({
|
root.tracking = false;
|
||||||
"offsetAlong": root.surface.isVertical ? root.velocityY : root.velocityX,
|
// Raised before dragging drops, so the lift holds through the fling.
|
||||||
"offsetCross": root.surface.isVertical ? root.velocityX : root.velocityY
|
host.flinging = true;
|
||||||
});
|
host.dragging = false;
|
||||||
root.hostWindow.storeAnchor();
|
// A release before the lift lands waits for it, or the fling would spring out of the old buffer.
|
||||||
root.hostWindow.wake();
|
if (host.lifted)
|
||||||
|
root.fling();
|
||||||
|
else
|
||||||
|
root.flingPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fling() {
|
||||||
|
const host = root.hostWindow;
|
||||||
|
root.flingPending = false;
|
||||||
|
// An expand requested mid-drag disables this handler; the sheet must not open with the release velocity.
|
||||||
|
if (root.surface.controller.expanded) {
|
||||||
|
host.flinging = false;
|
||||||
|
host.wake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const velocity = root.releaseVelocity;
|
||||||
|
const anchor = root.anchorFor(root.lastPointer);
|
||||||
|
root.surface.flingSeed = {
|
||||||
|
"offsetAlong": root.surface.isVertical ? velocity.y : velocity.x,
|
||||||
|
"offsetCross": root.surface.isVertical ? velocity.x : velocity.y
|
||||||
|
};
|
||||||
|
host.anchor = Qt.point(host.clampAnchorX(anchor.x + velocity.x * root.flingProjection), host.clampAnchorY(anchor.y + velocity.y * root.flingProjection));
|
||||||
|
// An unchanged anchor never reaches applyTarget, so the seed is spent here.
|
||||||
|
if (root.surface.flingSeed)
|
||||||
|
root.surface.applyTarget();
|
||||||
|
host.flinging = root.surface.motionRunning;
|
||||||
|
host.wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
parent: root.surface
|
parent: root.surface
|
||||||
@@ -76,6 +150,12 @@ DragHandler {
|
|||||||
if (!root.tracking)
|
if (!root.tracking)
|
||||||
root.rememberPress();
|
root.rememberPress();
|
||||||
if (active)
|
if (active)
|
||||||
root.updateDrag();
|
root.recordPointer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One anchor write per frame however fast the mouse polls, and none before the lift lands.
|
||||||
|
property FrameAnimation moveFrame: FrameAnimation {
|
||||||
|
running: root.active && root.movePending && root.hostWindow.lifted
|
||||||
|
onTriggered: root.applyMove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,22 +28,43 @@ PanelWindow {
|
|||||||
readonly property real screenHeight: root.screen?.height ?? 1080
|
readonly property real screenHeight: root.screen?.height ?? 1080
|
||||||
readonly property string positionKey: IslandHostRegistry.key(root.screen?.name, root.barId)
|
readonly property string positionKey: IslandHostRegistry.key(root.screen?.name, root.barId)
|
||||||
readonly property var storedAnchor: SessionData.islandFreePositions[root.positionKey] ?? null
|
readonly property var storedAnchor: SessionData.islandFreePositions[root.positionKey] ?? null
|
||||||
readonly property real storedAnchorX: root.clampAnchorX((typeof root.storedAnchor?.x === "number" ? root.storedAnchor.x : 0.5) * root.screenWidth)
|
readonly property point storedPoint: Qt.point(root.clampAnchorX((typeof root.storedAnchor?.x === "number" ? root.storedAnchor.x : 0.5) * root.screenWidth), root.clampAnchorY((typeof root.storedAnchor?.y === "number" ? root.storedAnchor.y : 0.5) * root.screenHeight))
|
||||||
readonly property real storedAnchorY: root.clampAnchorY((typeof root.storedAnchor?.y === "number" ? root.storedAnchor.y : 0.5) * root.screenHeight)
|
readonly property real anchorX: root.anchor.x
|
||||||
|
readonly property real anchorY: root.anchor.y
|
||||||
|
// Sized from the visual's size alone, so a fractional spring position never resizes the surface.
|
||||||
readonly property int surfaceX: Math.floor(body.currentVisualX - root.hitPadding - body.shadowPadding)
|
readonly property int surfaceX: Math.floor(body.currentVisualX - root.hitPadding - body.shadowPadding)
|
||||||
readonly property int surfaceY: Math.floor(body.currentVisualY - root.hitPadding - body.shadowPadding)
|
readonly property int surfaceY: Math.floor(body.currentVisualY - root.hitPadding - body.shadowPadding)
|
||||||
|
readonly property int surfaceWidth: Math.ceil(body.currentVisualWidth + 2 * (root.hitPadding + body.shadowPadding)) + 1
|
||||||
|
readonly property int surfaceHeight: Math.ceil(body.currentVisualHeight + 2 * (root.hitPadding + body.shadowPadding)) + 1
|
||||||
|
|
||||||
|
// Gestures and morphs run on a surface lifted to the whole output: per-frame margin commits skew pointer coordinates and relayout Hyprland.
|
||||||
|
property bool lifting: false
|
||||||
|
property bool liftForced: false
|
||||||
|
property point liftOrigin
|
||||||
|
// True from the unlift until the tight configure lands: a re-grab in that window still reads the output size.
|
||||||
|
property bool sizeStale: false
|
||||||
|
readonly property bool outputSized: root.width >= root.screenWidth - 2 && root.height >= root.screenHeight - 2
|
||||||
|
// Negative far margins grow the surface in place; the origin flips only once the buffer spans the output.
|
||||||
|
readonly property bool lifted: root.lifting && (root.liftForced || (!root.sizeStale && root.outputSized))
|
||||||
|
readonly property int frameLeft: root.lifted ? 0 : root.lifting ? root.liftOrigin.x : root.surfaceX
|
||||||
|
readonly property int frameTop: root.lifted ? 0 : root.lifting ? root.liftOrigin.y : root.surfaceY
|
||||||
|
readonly property int frameRight: root.lifting ? -root.frameLeft : Math.round(root.screenWidth) - root.surfaceX - root.surfaceWidth
|
||||||
|
readonly property int frameBottom: root.lifting ? -root.frameTop : Math.round(root.screenHeight) - root.surfaceY - root.surfaceHeight
|
||||||
|
|
||||||
readonly property bool expanded: body.expanded
|
readonly property bool expanded: body.expanded
|
||||||
readonly property bool pointerInside: body.islandController.pointerInside
|
readonly property bool pointerInside: body.islandController.pointerInside
|
||||||
readonly property string activeActivity: body.islandController.activeActivity
|
readonly property string activeActivity: body.islandController.activeActivity
|
||||||
// An idle-shrunk nub keeps its full-size hit area; the drag area shares it or ring clicks would be lost.
|
readonly property bool motionRunning: body.motionRunning
|
||||||
readonly property real hitPadding: root.idle ? Math.max(0, (root.dotSize - root.dotSize * root.idleScale) / 2) : 0
|
// A shrunk nub keeps the full dot's hit area and surface, so idling and waking never resize the window.
|
||||||
|
readonly property real hitPadding: root.dot ? Math.max(0, (root.dotSize - body.currentVisualWidth) / 2) : 0
|
||||||
// Blur regions are not antialiased and a small circle shows the jagged rim, so blur waits for the morph.
|
// Blur regions are not antialiased and a small circle shows the jagged rim, so blur waits for the morph.
|
||||||
readonly property bool blurWanted: !root.dot || Math.min(body.currentVisualWidth, body.currentVisualHeight) > root.dotSize * 1.5
|
readonly property bool blurWanted: !root.dot || Math.min(body.currentVisualWidth, body.currentVisualHeight) > root.dotSize * 1.5
|
||||||
|
|
||||||
property real anchorX: 0
|
property point anchor
|
||||||
property real anchorY: 0
|
|
||||||
property bool dragging: false
|
property bool dragging: false
|
||||||
|
property bool flinging: false
|
||||||
|
// sheetOut spans the expand through the collapse spring, so the whole morph draws into one static buffer.
|
||||||
|
readonly property bool liftWanted: root.dragging || root.flinging || body.sheetOut
|
||||||
property bool idle: false
|
property bool idle: false
|
||||||
|
|
||||||
function anchorLimit(size, extent) {
|
function anchorLimit(size, extent) {
|
||||||
@@ -64,16 +85,24 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function storeAnchor() {
|
function storeAnchor() {
|
||||||
SessionData.setIslandFreePosition(root.positionKey, root.anchorX / root.screenWidth, root.anchorY / root.screenHeight);
|
SessionData.setIslandFreePosition(root.positionKey, root.anchor.x / root.screenWidth, root.anchor.y / root.screenHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveTo(x, y) {
|
function moveTo(x, y) {
|
||||||
root.anchorX = root.clampAnchorX(x);
|
root.anchor = Qt.point(root.clampAnchorX(x), root.clampAnchorY(y));
|
||||||
root.anchorY = root.clampAnchorY(y);
|
|
||||||
root.storeAnchor();
|
root.storeAnchor();
|
||||||
root.wake();
|
root.wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function beginLift() {
|
||||||
|
if (root.lifting)
|
||||||
|
return;
|
||||||
|
root.liftOrigin = Qt.point(root.frameLeft, root.frameTop);
|
||||||
|
root.liftForced = false;
|
||||||
|
root.lifting = true;
|
||||||
|
liftTimeout.restart();
|
||||||
|
}
|
||||||
|
|
||||||
function wake() {
|
function wake() {
|
||||||
root.idle = false;
|
root.idle = false;
|
||||||
if (root.idleDelay > 0)
|
if (root.idleDelay > 0)
|
||||||
@@ -90,17 +119,37 @@ PanelWindow {
|
|||||||
return body.containsGlobalPoint(gx, gy, padding);
|
return body.containsGlobalPoint(gx, gy, padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStoredAnchorXChanged: {
|
onStoredPointChanged: {
|
||||||
if (!root.dragging)
|
if (!root.liftWanted)
|
||||||
root.anchorX = root.storedAnchorX;
|
root.anchor = root.storedPoint;
|
||||||
}
|
}
|
||||||
onStoredAnchorYChanged: {
|
// Stored once the fling settles; a save mid-gesture stalls the fling's first frame.
|
||||||
if (!root.dragging)
|
onLiftWantedChanged: {
|
||||||
root.anchorY = root.storedAnchorY;
|
if (root.liftWanted) {
|
||||||
|
root.beginLift();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.sizeStale = root.outputSized;
|
||||||
|
root.lifting = false;
|
||||||
|
liftTimeout.stop();
|
||||||
|
root.storeAnchor();
|
||||||
|
}
|
||||||
|
onOutputSizedChanged: {
|
||||||
|
if (!root.outputSized)
|
||||||
|
root.sizeStale = false;
|
||||||
|
}
|
||||||
|
onLiftedChanged: {
|
||||||
|
if (root.lifted)
|
||||||
|
liftTimeout.stop();
|
||||||
|
}
|
||||||
|
onMotionRunningChanged: {
|
||||||
|
if (!root.motionRunning)
|
||||||
|
root.flinging = false;
|
||||||
}
|
}
|
||||||
onPointerInsideChanged: root.wake()
|
onPointerInsideChanged: root.wake()
|
||||||
onExpandedChanged: root.wake()
|
onExpandedChanged: root.wake()
|
||||||
onActiveActivityChanged: root.wake()
|
onActiveActivityChanged: root.wake()
|
||||||
|
onIdleDelayChanged: root.wake()
|
||||||
|
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusiveZone: -1
|
exclusiveZone: -1
|
||||||
@@ -122,16 +171,19 @@ PanelWindow {
|
|||||||
radius: body.currentSurfaceRadius > 0 ? Math.ceil(body.currentSurfaceRadius) + 1 : 0
|
radius: body.currentSurfaceRadius > 0 ? Math.ceil(body.currentSurfaceRadius) + 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All four edges are anchored so the margins alone place and size the surface, letting a lift grow it past the output.
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
|
bottom: true
|
||||||
left: true
|
left: true
|
||||||
|
right: true
|
||||||
}
|
}
|
||||||
WlrLayershell.margins {
|
WlrLayershell.margins {
|
||||||
left: root.surfaceX
|
left: root.frameLeft
|
||||||
top: root.surfaceY
|
top: root.frameTop
|
||||||
|
right: root.frameRight
|
||||||
|
bottom: root.frameBottom
|
||||||
}
|
}
|
||||||
implicitWidth: Math.ceil(body.currentVisualX + body.currentVisualWidth + root.hitPadding + body.shadowPadding) - root.surfaceX
|
|
||||||
implicitHeight: Math.ceil(body.currentVisualY + body.currentVisualHeight + root.hitPadding + body.shadowPadding) - root.surfaceY
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
root.wake();
|
root.wake();
|
||||||
@@ -150,24 +202,46 @@ PanelWindow {
|
|||||||
Timer {
|
Timer {
|
||||||
id: idleTimer
|
id: idleTimer
|
||||||
|
|
||||||
interval: Math.max(1000, root.idleDelay)
|
interval: root.idleDelay
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root.expanded || root.pointerInside || root.dragging || body.islandController.transientActive)
|
if (root.expanded || root.pointerInside || hitHover.hovered || root.liftWanted || body.islandController.transientActive)
|
||||||
return;
|
return;
|
||||||
root.idle = true;
|
root.idle = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mask: Region {
|
// A compositor that never sizes the lifted surface would leave every drag frozen.
|
||||||
|
Timer {
|
||||||
|
id: liftTimeout
|
||||||
|
|
||||||
|
interval: 250
|
||||||
|
onTriggered: root.liftForced = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// A quick stroke outruns the dot's rectangle between frames and the pointer leave ends the drag, so a drag takes input everywhere.
|
||||||
|
mask: root.dragging && root.lifted ? null : hitRegion
|
||||||
|
Region {
|
||||||
|
id: hitRegion
|
||||||
|
|
||||||
item: body.inputSuspended ? null : hitTarget
|
item: body.inputSuspended ? null : hitTarget
|
||||||
}
|
}
|
||||||
Item {
|
Item {
|
||||||
id: hitTarget
|
id: hitTarget
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: body.shadowPadding
|
x: body.currentVisualX - root.hitPadding - root.frameLeft
|
||||||
|
y: body.currentVisualY - root.hitPadding - root.frameTop
|
||||||
|
width: body.currentVisualWidth + 2 * root.hitPadding
|
||||||
|
height: body.currentVisualHeight + 2 * root.hitPadding
|
||||||
|
|
||||||
|
// The idle ring around a shrunk nub takes clicks and drags, so hovering it wakes the dot too.
|
||||||
|
HoverHandler {
|
||||||
|
id: hitHover
|
||||||
|
|
||||||
|
onHoveredChanged: root.wake()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the animated buffer tight; the unmoving input-only window catches outside clicks.
|
// Catches outside clicks; the lifted surface's own mask stays the island rectangle.
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: dismissWindow
|
id: dismissWindow
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
@@ -182,14 +256,17 @@ PanelWindow {
|
|||||||
left: true
|
left: true
|
||||||
right: true
|
right: true
|
||||||
}
|
}
|
||||||
|
// The frozen spring envelope, so this full-output surface is not re-committed every frame of a morph.
|
||||||
mask: Region {
|
mask: Region {
|
||||||
width: dismissWindow.width
|
width: dismissWindow.width
|
||||||
height: dismissWindow.height
|
height: dismissWindow.height
|
||||||
Region {
|
Region {
|
||||||
x: root.surfaceX + hitTarget.x
|
readonly property Item envelope: body.inputMaskItem
|
||||||
y: root.surfaceY + hitTarget.y
|
|
||||||
width: hitTarget.width
|
x: dismissWindow.visible ? envelope.x : 0
|
||||||
height: hitTarget.height
|
y: dismissWindow.visible ? envelope.y : 0
|
||||||
|
width: dismissWindow.visible ? envelope.width : 0
|
||||||
|
height: dismissWindow.visible ? envelope.height : 0
|
||||||
intersection: Intersection.Subtract
|
intersection: Intersection.Subtract
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,8 +289,8 @@ PanelWindow {
|
|||||||
IslandBarHost {
|
IslandBarHost {
|
||||||
id: body
|
id: body
|
||||||
|
|
||||||
x: -root.surfaceX
|
x: -root.frameLeft
|
||||||
y: -root.surfaceY
|
y: -root.frameTop
|
||||||
width: root.screenWidth
|
width: root.screenWidth
|
||||||
height: root.screenHeight
|
height: root.screenHeight
|
||||||
originOffsetX: x
|
originOffsetX: x
|
||||||
@@ -221,9 +298,9 @@ PanelWindow {
|
|||||||
barConfig: root.barConfig
|
barConfig: root.barConfig
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
hostWindow: root
|
hostWindow: root
|
||||||
|
hostLifted: root.lifted
|
||||||
barId: root.barId
|
barId: root.barId
|
||||||
anchorX: root.anchorX
|
anchor: root.anchor
|
||||||
anchorY: root.anchorY
|
|
||||||
anchorSnaps: root.dragging
|
anchorSnaps: root.dragging
|
||||||
freeScale: root.idle ? root.idleScale : 1
|
freeScale: root.idle ? root.idleScale : 1
|
||||||
freeOpacity: root.idle ? root.idleOpacity : 1
|
freeOpacity: root.idle ? root.idleOpacity : 1
|
||||||
|
|||||||
@@ -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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user