Compare commits

...
31 Commits
Author SHA1 Message Date
Guillem CANAL 668dde2a33 idle: retry suspend when a logind sleep inhibitor is released (#3866) 2026-10-22 17:12:36 -04:00
Mario Mohar 900e6110d9 wallpaper: push the cycling config when the service is created (#3874)
port 2.0
2026-10-22 17:09:02 -04:00
bbedward 44d498e362 bar: drop pill blur region when a vertical pill collapses to zero height
fixes #3873
port 2.0
2026-10-22 17:00:53 -04:00
bbedward bf9a51c448 i18n: sync 2026-10-22 15:41:11 -04:00
bbedward 1a46db0517 i18n: sync 2026-10-22 15:13:00 -04:00
alvarosth 7781c207f1 Add opt-in administrator lock restrictions (#3869) 2026-10-22 14:57:29 -04:00
bbedward 5b7777785a shell: restore QT disable decoration pragma 2026-10-22 14:51:36 -04:00
Vaibhav Pratap Singh 0aab0ec09b battery: pulse only the icon when the bar has no background (#3861) 2026-10-22 12:13:48 -04:00
bbedward da79eca3c0 ci: disable background tasks in review job 2026-10-22 11:43:54 -04:00
bbedward aeacaf7afc distro/freebsd: drop elogind references 2026-10-22 10:38:15 -04:00
bbedward 14b3ddaf83 plugins: label declared permissions as unenforced 2026-10-22 09:44:13 -04:00
bbedward 6e86b53ccd plugins: keep manifest paths inside the plugin directory 2026-10-22 09:44:08 -04:00
bbedward c5d1d4fe46 greeter: remove the embedded legacy greeter (again) 2026-10-22 09:37:07 -04:00
purian23 39a58f77ec distos: compare ubuntu published commit by prefix 2026-10-22 06:06:38 -04:00
purian23 47c9d8d89b notifications: keep popup content while it exits after the notification closes
Port 2.0
2026-10-21 21:58:13 -04:00
bbedward d8786eaaf7 bar: keep far-edge island hosts at sheet size so the bar never jumps on expand
fixes #3855
port 2.0
2026-10-21 21:57:20 -04:00
Anton Kesy 4f969c8e10 docs: use dux project names in README (#3847) 2026-10-21 15:50:06 -04:00
bbedward 1b59a474dd battery: throttle pulse animation to 20fps
ref #3849
2026-10-21 13:59:43 -04:00
bbedward 28f921fa02 dash: hold panel height while dragging cards unless hung from a top bar
lyrics: ignore floored position samples and shell refresh emits
2026-10-21 13:15:53 -04:00
bbedward 8f2a486006 theme: publish typography for dux apps 2026-10-21 12:26:05 -04:00
bbedward 27a703367f core: only use the simple animation driver for the scrolling wallpaper
fixes #3837
2026-10-21 11:18:18 -04:00
bbedward 8e851b3dc4 bar: slide an auto-hidden island by its compact height
fixes #3838
2026-10-21 11:05:57 -04:00
bbedward 112d05901f workspaces: keep the pill highlight while hovering its app icons
fixes #3839
2026-10-21 10:39:40 -04:00
bbedward afeea489c9 theme: skip matugen runs whose inputs match the last applied run
fixes #3841
2026-10-21 10:34:22 -04:00
bbedward 90d3c9731c niri: only emit displayScales when a scale changed
ref #3842
port 2.0
2026-10-21 10:14:52 -04:00
Marlon Kranz b81954454a fix(notifications): close popups when their screen stops showing them (#3844) 2026-10-21 08:02:49 -04:00
Adarsh 479b1434de Theme Antigravity IDE via the VS Code extension (#3836) 2026-10-21 03:57:45 -04:00
Tomás 203b1369b3 Keep MangoWM workspace apps on their own monitor (#3832)
port 2.0
2026-10-20 20:54:58 -04:00
Leonard 5ffc581ecf refactor(ui): match the dock settings page with the bar settings page (#3829)
port 2.0
2026-10-20 20:54:40 -04:00
bbedward 3284acab28 matugen: neovim template reads the dms config dir"
port 2.0
2026-10-20 17:03:12 -04:00
bbedward 1fb9f5271a ci: cache go modules from core/go.sum, retry downloads 2026-10-20 15:24:59 -04:00
162 changed files with 5983 additions and 8147 deletions
+2
View File
@@ -71,6 +71,8 @@ jobs:
run: echo "base=$(gh pr view ${{ github.event.issue.number }} --json baseRefName --jq .baseRefName)" >> "$GITHUB_OUTPUT" run: echo "base=$(gh pr view ${{ github.event.issue.number }} --json baseRefName --jq .baseRefName)" >> "$GITHUB_OUTPUT"
- uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507 - uses: anthropics/claude-code-action@9ca9355b36297178e28d37c799d1c9c8a28e6507
env:
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1"
id: claude id: claude
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+5
View File
@@ -26,6 +26,11 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: core/go.mod go-version-file: core/go.mod
cache-dependency-path: core/go.sum
- name: Download Go modules
working-directory: core
run: for attempt in 1 2 3; do go mod download && exit 0; sleep 15; done; exit 1
- name: run pre-commit hooks - name: run pre-commit hooks
uses: j178/prek-action@v2 uses: j178/prek-action@v2
+2
View File
@@ -73,6 +73,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Format check - name: Format check
if: matrix.goos == 'linux' if: matrix.goos == 'linux'
@@ -247,6 +248,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v5 uses: actions/download-artifact@v5
+1
View File
@@ -308,6 +308,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
- name: Install OSC - name: Install OSC
run: | run: |
+1
View File
@@ -100,6 +100,7 @@ jobs:
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache-dependency-path: core/go.sum
cache: false cache: false
- name: Install build dependencies - name: Install build dependencies
+7 -7
View File
@@ -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
+34 -1
View File
@@ -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) {
+78 -13
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@@ -14,11 +15,28 @@ const (
) )
type File struct { type File struct {
PolicyVersion int `json:"policy_version"` PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"` ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"` BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"` Message *string `json:"message"`
DisableRegistries *bool `json:"disable_registries"` DisableRegistries *bool `json:"disable_registries"`
DisableLockIPCUnlock *bool `json:"disable_lock_ipc_unlock"`
DisableLogindUnlock *bool `json:"disable_logind_unlock"`
DisableCustomLockPAM *bool `json:"disable_custom_lock_pam"`
DisableUserPlugins *bool `json:"disable_user_plugins"`
DisableSettingsIPCSet *bool `json:"disable_settings_ipc_set"`
}
// shellSwitches reach the shell as DMS_POLICY_<KEY> in its launch environment.
var shellSwitches = []struct {
key string
value func(*File) *bool
}{
{"disable_lock_ipc_unlock", func(p *File) *bool { return p.DisableLockIPCUnlock }},
{"disable_logind_unlock", func(p *File) *bool { return p.DisableLogindUnlock }},
{"disable_custom_lock_pam", func(p *File) *bool { return p.DisableCustomLockPAM }},
{"disable_user_plugins", func(p *File) *bool { return p.DisableUserPlugins }},
{"disable_settings_ipc_set", func(p *File) *bool { return p.DisableSettingsIPCSet }},
} }
// LoadFile returns nil without an error when the file does not exist. // LoadFile returns nil without an error when the file does not exist.
@@ -39,18 +57,65 @@ func LoadFile(fs afero.Fs, path string) (*File, error) {
return &policy, nil return &policy, nil
} }
// RegistriesDisabled reports whether the policy turns off the plugin and theme // load returns the policy files that exist, PackagedPath first.
// registries. An explicit value in AdminPath overrides PackagedPath. func load(fs afero.Fs) ([]*File, error) {
func RegistriesDisabled(fs afero.Fs) (bool, error) { var policies []*File
disabled := false
for _, path := range []string{PackagedPath, AdminPath} { for _, path := range []string{PackagedPath, AdminPath} {
policy, err := LoadFile(fs, path) policy, err := LoadFile(fs, path)
if err != nil { if err != nil {
return false, err return nil, err
} }
if policy != nil && policy.DisableRegistries != nil { if policy != nil {
disabled = *policy.DisableRegistries policies = append(policies, policy)
} }
} }
return disabled, nil return policies, nil
}
// flag reports a boolean key, false unless set. An explicit value in AdminPath
// overrides PackagedPath.
func flag(policies []*File, value func(*File) *bool) bool {
set := false
for _, policy := range policies {
if v := value(policy); v != nil {
set = *v
}
}
return set
}
// RegistriesDisabled reports whether the policy turns off the plugin and theme
// registries. An explicit value in AdminPath overrides PackagedPath.
func RegistriesDisabled(fs afero.Fs) (bool, error) {
policies, err := load(fs)
if err != nil {
return false, err
}
return flag(policies, func(p *File) *bool { return p.DisableRegistries }), nil
}
// LogindUnlockDisabled reports whether a logind Unlock must leave a lock in
// place. An explicit value in AdminPath overrides PackagedPath.
func LogindUnlockDisabled(fs afero.Fs) (bool, error) {
policies, err := load(fs)
if err != nil {
return false, err
}
return flag(policies, func(p *File) *bool { return p.DisableLogindUnlock }), nil
}
// ShellEnv returns every shell switch as DMS_POLICY_<KEY>=1 or 0, so a value
// inherited by the shell never counts. A policy file that fails to load turns
// every switch on, and its error is returned with them.
func ShellEnv(fs afero.Fs) ([]string, error) {
policies, err := load(fs)
env := make([]string, 0, len(shellSwitches))
for _, s := range shellSwitches {
value := "0"
if err != nil || flag(policies, s.value) {
value = "1"
}
env = append(env, "DMS_POLICY_"+strings.ToUpper(s.key)+"="+value)
}
return env, err
} }
+101
View File
@@ -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
View File
@@ -965,6 +965,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"},
} }
+3 -1
View File
@@ -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)
+16 -7
View File
@@ -30,13 +30,22 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
} }
case dbusSessionInterface + ".Unlock": case dbusSessionInterface + ".Unlock":
lockedHint := m.sessionLockedHint() if m.keepLockOnUnlock {
m.stateMutex.Lock() // Only the locker clears LockedHint, so take both from logind, and
m.state.Locked = false // keep the cached state when logind cannot be read.
m.state.LockedHint = lockedHint if m.sessionObj != nil && m.updateSessionState() == nil && !m.snapshotState().Locked {
m.stateMutex.Unlock() m.prelockedReady.Store(false)
m.notifySubscribers() }
m.prelockedReady.Store(false) m.notifySubscribers()
} else {
lockedHint := m.sessionLockedHint()
m.stateMutex.Lock()
m.state.Locked = false
m.state.LockedHint = lockedHint
m.stateMutex.Unlock()
m.notifySubscribers()
m.prelockedReady.Store(false)
}
// Cancel the lock timer if it's still running // Cancel the lock timer if it's still running
m.lockTimerMu.Lock() m.lockTimerMu.Lock()
+30 -14
View File
@@ -37,42 +37,58 @@ func TestManager_HandleDBusSignal_Lock(t *testing.T) {
func TestManager_HandleDBusSignal_Unlock(t *testing.T) { func TestManager_HandleDBusSignal_Unlock(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
lockedHint bool keepLock bool
call *dbus.Call // logind's GetAll answer; nil means no session object
wantLocked bool
wantLockedHint bool
}{ }{
{name: "logind cleared LockedHint", lockedHint: false}, {name: "logind cleared LockedHint", call: lockedHintCall(false)},
{name: "logind still reports LockedHint", lockedHint: true}, {name: "logind still reports LockedHint", call: lockedHintCall(true), wantLockedHint: true},
{name: "logind unreachable", call: &dbus.Call{Err: assert.AnError}},
{name: "policy, LockedHint still set", keepLock: true, call: lockedHintCall(true), wantLocked: true, wantLockedHint: true},
{name: "policy, LockedHint cleared", keepLock: true, call: lockedHintCall(false)},
{name: "policy, logind unreachable", keepLock: true, call: &dbus.Call{Err: assert.AnError}, wantLocked: true, wantLockedHint: true},
{name: "policy, no session object", keepLock: true, wantLocked: true, wantLockedHint: true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
sessionObj := mockdbus.NewMockBusObject(t)
sessionObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.login1.Session").Return(&dbus.Call{
Body: []any{map[string]dbus.Variant{"LockedHint": dbus.MakeVariant(tt.lockedHint)}},
})
manager := &Manager{ manager := &Manager{
state: &SessionState{ state: &SessionState{
Locked: true, Locked: true,
LockedHint: true, LockedHint: true,
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
sessionObj: sessionObj, keepLockOnUnlock: tt.keepLock,
} }
if tt.call != nil {
sessionObj := mockdbus.NewMockBusObject(t)
sessionObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.login1.Session").Return(tt.call)
manager.sessionObj = sessionObj
}
manager.markLockerReady()
manager.handleDBusSignal(&dbus.Signal{ manager.handleDBusSignal(&dbus.Signal{
Name: "org.freedesktop.login1.Session.Unlock", Name: "org.freedesktop.login1.Session.Unlock",
}) })
assert.Equal(t, tt.wantLocked, manager.prelockedReady.Load())
manager.stateMutex.RLock() manager.stateMutex.RLock()
defer manager.stateMutex.RUnlock() defer manager.stateMutex.RUnlock()
assert.False(t, manager.state.Locked) assert.Equal(t, tt.wantLocked, manager.state.Locked)
assert.Equal(t, tt.lockedHint, manager.state.LockedHint) assert.Equal(t, tt.wantLockedHint, manager.state.LockedHint)
}) })
} }
} }
func lockedHintCall(lockedHint bool) *dbus.Call {
return &dbus.Call{Body: []any{map[string]dbus.Variant{
"LockedHint": dbus.MakeVariant(lockedHint),
}}}
}
func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) { func TestManager_HandleDBusSignal_PrepareForSleep(t *testing.T) {
t.Run("preparing for sleep - true", func(t *testing.T) { t.Run("preparing for sleep - true", func(t *testing.T) {
manager := &Manager{ manager := &Manager{
+1
View File
@@ -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
} }
+8 -1
View File
@@ -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
+5 -8
View File
@@ -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`
+3 -3
View File
@@ -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
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": { "dank-qml-common": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1792513666, "lastModified": 1792696287,
"narHash": "sha256-RDqPdPrM5lnwRrfFGLz3IwgTjCV2Ie+nrTD3ldj1t/s=", "narHash": "sha256-htrNweCLuw+3TQA2Vm7b4ETCuypIx85Vb3jt6k/DdBw=",
"owner": "duxlabs-dev", "owner": "duxlabs-dev",
"repo": "dux-qml-common", "repo": "dux-qml-common",
"rev": "ebf5e6c002da1bdaa4aaa2f319fea7b8507abc42", "rev": "abd63f1d329cbffd33ad0d28420944353bbf50ed",
"type": "github" "type": "github"
}, },
"original": { "original": {
+12
View File
@@ -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"
}
+3 -11
View File
@@ -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");
} }
} }
+5 -52
View File
@@ -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
+10 -88
View File
@@ -25,8 +25,6 @@ Singleton {
readonly property int settingsConfigVersion: 42 readonly property int settingsConfigVersion: 42
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
enum Position { enum Position {
Top, Top,
Bottom, Bottom,
@@ -1435,8 +1433,6 @@ Singleton {
signal notificationPopupsInvalidated signal notificationPopupsInvalidated
function refreshAuthAvailability() { function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.detectAuthCapabilities(); Processes.detectAuthCapabilities();
} }
@@ -1506,8 +1502,6 @@ Singleton {
} }
function checkIconThemeDrift() { function checkIconThemeDrift() {
if (isGreeterMode)
return;
if (resolveIconTheme() === "System Default") if (resolveIconTheme() === "System Default")
return; return;
if (!SessionData.lastAppliedIconTheme) if (!SessionData.lastAppliedIconTheme)
@@ -1664,8 +1658,6 @@ Singleton {
} }
function scheduleAuthApply() { function scheduleAuthApply() {
if (isGreeterMode)
return;
Qt.callLater(() => { Qt.callLater(() => {
Processes.settingsRoot = root; Processes.settingsRoot = root;
Processes.scheduleAuthApply(); Processes.scheduleAuthApply();
@@ -1673,8 +1665,6 @@ Singleton {
} }
function scheduleGreeterAutoLoginSync() { function scheduleGreeterAutoLoginSync() {
if (isGreeterMode)
return;
Qt.callLater(() => { Qt.callLater(() => {
Processes.settingsRoot = root; Processes.settingsRoot = root;
Processes.scheduleGreeterAutoLoginSync(); Processes.scheduleGreeterAutoLoginSync();
@@ -1689,8 +1679,6 @@ Singleton {
} }
function markGreeterSyncPending(who, key, oldValue) { function markGreeterSyncPending(who, key, oldValue) {
if (isGreeterMode)
return;
if (!(key in SessionData.greeterSyncBaseline)) { if (!(key in SessionData.greeterSyncBaseline)) {
var baseline = Object.assign({}, SessionData.greeterSyncBaseline); var baseline = Object.assign({}, SessionData.greeterSyncBaseline);
baseline[key] = oldValue; baseline[key] = oldValue;
@@ -1840,7 +1828,7 @@ Singleton {
let loadedSettings = JSON.stringify(obj); let loadedSettings = JSON.stringify(obj);
const oldVersion = obj?.configVersion ?? 0; const oldVersion = obj?.configVersion ?? 0;
if (oldVersion > settingsConfigVersion && !isGreeterMode) { if (oldVersion > settingsConfigVersion) {
newerSettingsBackup.path = `${_configDir}/dms/settings.v${oldVersion}.json`; newerSettingsBackup.path = `${_configDir}/dms/settings.v${oldVersion}.json`;
newerSettingsBackup.setText(JSON.stringify(obj, null, 2)); newerSettingsBackup.setText(JSON.stringify(obj, null, 2));
} }
@@ -2063,16 +2051,14 @@ Singleton {
return splitSettings; return splitSettings;
} }
function saveSettings() { function saveSettings() {
if (isGreeterMode || _loading || !_hasLoaded) if (_loading || !_hasLoaded)
return; return;
unsavedUserChanges = true; unsavedUserChanges = true;
settingsSaveDebounce.restart(); settingsSaveDebounce.restart();
} }
function _saveSettings(userChanges) { function _saveSettings(userChanges) {
let reason = null; let reason = null;
if (isGreeterMode) { if (_loading) {
reason = "running in greeter mode.";
} else if (_loading) {
reason = "some files are being loaded"; reason = "some files are being loaded";
} else if (_parseError) { } else if (_parseError) {
reason = "failed to parse settings."; reason = "failed to parse settings.";
@@ -2092,7 +2078,7 @@ Singleton {
} }
function savePluginSettings() { function savePluginSettings() {
if (isGreeterMode || _pluginSettingsLoading || _pluginParseError) if (_pluginSettingsLoading || _pluginParseError)
return; return;
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2)); pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
} }
@@ -2402,7 +2388,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;
@@ -3725,11 +3711,11 @@ Singleton {
property FileView fileView: FileView { property FileView fileView: FileView {
id: settingsFileView id: settingsFileView
path: isGreeterMode ? "" : filePath path: filePath
blockLoading: false blockLoading: false
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: true
onFileChanged: { onFileChanged: {
if (selfWrite) { if (selfWrite) {
selfWrite = false; selfWrite = false;
@@ -3740,9 +3726,6 @@ Singleton {
} }
} }
onLoaded: { onLoaded: {
if (isGreeterMode) {
return;
}
isLoading = true; isLoading = true;
_loading = true; _loading = true;
const hadParseFailed = hasParseFailed; const hadParseFailed = hasParseFailed;
@@ -3776,9 +3759,6 @@ Singleton {
} }
} }
onLoadFailed: error => { onLoadFailed: error => {
if (isGreeterMode) {
return;
}
isLoading = false; isLoading = false;
_loading = _anySettingsFile(file => file.isLoading); _loading = _anySettingsFile(file => file.isLoading);
if (error === FileViewError.FileNotFound) { if (error === FileViewError.FileNotFound) {
@@ -3922,7 +3902,7 @@ Singleton {
} }
property bool _settingsFolderExists: false property bool _settingsFolderExists: false
property bool _settingsFolderChecked: isGreeterMode property bool _settingsFolderChecked: false
function _syncSettingsFilesModels() { function _syncSettingsFilesModels() {
if (!_settingsFolderChecked) { if (!_settingsFolderChecked) {
settingsFilesModelSyncDebounce.restart(); settingsFilesModelSyncDebounce.restart();
@@ -3963,7 +3943,6 @@ Singleton {
} }
Loader { Loader {
id: settingsFolderLoader id: settingsFolderLoader
active: !isGreeterMode
function unloadModel() { function unloadModel() {
active = false; active = false;
} }
@@ -4012,59 +3991,6 @@ Singleton {
} }
} }
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string greeterSettingsBaseDir: root._greeterCacheDir
function setGreeterSettingsBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (greeterSettingsBaseDir === next)
return;
greeterSettingsBaseDir = next;
if (isGreeterMode)
greeterSettingsFile.reload();
}
function resetGreeterSettingsBaseDir() {
setGreeterSettingsBaseDir(root._greeterCacheDir);
}
function loadGreeterSettings(txt) {
_loading = true;
_hasLoaded = false;
try {
Store.parse(root, (txt && txt.trim()) ? JSON.parse(txt) : {});
_parseError = false;
} catch (e) {
_parseError = true;
log.error("Failed to parse greeter settings.json:", e.message);
Store.parse(root, {});
} finally {
_loading = false;
}
_hasLoaded = true;
applyStoredTheme();
}
FileView {
id: greeterSettingsFile
path: isGreeterMode && root.greeterSettingsBaseDir ? (root.greeterSettingsBaseDir + "/settings.json") : ""
preload: isGreeterMode
blockLoading: false
blockWrites: true
watchChanges: false
printErrors: false
onLoaded: {
if (isGreeterMode)
loadGreeterSettings(greeterSettingsFile.text());
}
onLoadFailed: {
if (isGreeterMode)
loadGreeterSettings("");
}
}
// Our first save drops the keys a newer shell wrote, so its settings are kept for going back. // Our first save drops the keys a newer shell wrote, so its settings are kept for going back.
FileView { FileView {
id: newerSettingsBackup id: newerSettingsBackup
@@ -4078,21 +4004,17 @@ Singleton {
FileView { FileView {
id: pluginSettingsFile id: pluginSettingsFile
path: isGreeterMode ? "" : pluginSettingsPath path: pluginSettingsPath
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
printErrors: false printErrors: false
watchChanges: !isGreeterMode watchChanges: true
onLoaded: { onLoaded: {
if (isGreeterMode)
return;
pluginSettingsFileExists = true; pluginSettingsFileExists = true;
parsePluginSettings(pluginSettingsFile.text()); parsePluginSettings(pluginSettingsFile.text());
} }
onLoadFailed: error => { onLoadFailed: error => {
if (isGreeterMode)
return;
const msg = String(error || ""); const msg = String(error || "");
const missing = _isMissingPluginSettingsError(error); const missing = _isMissingPluginSettingsError(error);
pluginSettingsFileExists = !missing; pluginSettingsFileExists = !missing;
+3
View File
@@ -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,
+45 -53
View File
@@ -13,7 +13,6 @@ import Quickshell.Io
import qs.Common import qs.Common
import qs.DCommon.Common as DCommon import qs.DCommon.Common as DCommon
import qs.Services import qs.Services
import qs.Modules.Greetd
import "StockThemes.js" as StockThemes import "StockThemes.js" as StockThemes
import "GSettings.js" as GSettings import "GSettings.js" as GSettings
@@ -1191,13 +1190,7 @@ Singleton {
readonly property real groupedListInnerRadius: cornerRadiusXS readonly property real groupedListInnerRadius: cornerRadiusXS
readonly property real groupedListOuterRadius: cornerRadiusL readonly property real groupedListOuterRadius: cornerRadiusL
property string fontFamily: { property string fontFamily: typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DCommon.Fonts.sans
if (typeof SettingsData === "undefined")
return DCommon.Fonts.sans;
if (SettingsData.isGreeterMode && SettingsData.lockScreenFontFamily !== "")
return resolvedFontFamily(SettingsData.lockScreenFontFamily);
return resolvedFontFamily(SettingsData.fontFamily);
}
property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DCommon.Fonts.mono property string monoFontFamily: typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DCommon.Fonts.mono
property string displayFontFamily: typeof SettingsData !== "undefined" ? resolvedDisplayFontFamily(SettingsData.displayFontFamily) : DCommon.Fonts.display property string displayFontFamily: typeof SettingsData !== "undefined" ? resolvedDisplayFontFamily(SettingsData.displayFontFamily) : DCommon.Fonts.display
@@ -1422,15 +1415,12 @@ Singleton {
currentThemeCategory = "generic"; currentThemeCategory = "generic";
} }
} }
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (savePrefs && typeof SettingsData !== "undefined") {
if (savePrefs && typeof SettingsData !== "undefined" && !isGreeterMode) {
SettingsData.set("currentThemeCategory", currentThemeCategory); SettingsData.set("currentThemeCategory", currentThemeCategory);
SettingsData.set("currentThemeName", currentTheme); SettingsData.set("currentThemeName", currentTheme);
} }
if (!isGreeterMode) { generateSystemThemesFromCurrentTheme();
generateSystemThemesFromCurrentTheme();
}
} }
function setLightMode(light, savePrefs = true, enableTransition = false) { function setLightMode(light, savePrefs = true, enableTransition = false) {
@@ -1447,17 +1437,14 @@ Singleton {
return; return;
} }
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (savePrefs && typeof SessionData !== "undefined") {
if (savePrefs && typeof SessionData !== "undefined" && !isGreeterMode) {
SessionData.setLightMode(light); SessionData.setLightMode(light);
} }
if (!isGreeterMode) { if (typeof SettingsData !== "undefined") {
if (typeof SettingsData !== "undefined") { SettingsData.updateCosmicThemeMode(light);
SettingsData.updateCosmicThemeMode(light);
}
generateSystemThemesFromCurrentTheme();
} }
generateSystemThemesFromCurrentTheme();
} }
function toggleLightMode(savePrefs = true) { function toggleLightMode(savePrefs = true) {
@@ -1876,9 +1863,9 @@ Singleton {
} }
const runKey = Qt.md5(JSON.stringify(args)); const runKey = Qt.md5(JSON.stringify(args));
const skipUnchanged = !_matugenRunKey && !colorsFileLoadFailed && typeof SettingsData !== "undefined" && !SettingsData.generateThemeAtStartup; const startupRun = !_matugenRunKey && typeof SettingsData !== "undefined" && SettingsData.generateThemeAtStartup;
if (skipUnchanged && runKey === CacheData.matugenAppliedKey) { if (!startupRun && !colorsFileLoadFailed && runKey === CacheData.matugenAppliedKey) {
log.info("Theme inputs unchanged since the last run, skipping startup generation"); log.info("Theme inputs unchanged since the last run, skipping generation");
return; return;
} }
@@ -1938,8 +1925,7 @@ Singleton {
} }
function generateSystemThemesFromCurrentTheme() { function generateSystemThemesFromCurrentTheme() {
const isGreeterMode = (typeof SessionData !== "undefined" && SessionData.isGreeterMode); if (!matugenAvailable)
if (!matugenAvailable || isGreeterMode)
return; return;
_pendingGenerateParams = true; _pendingGenerateParams = true;
@@ -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"
+1 -1
View File
@@ -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) {
+7 -7
View File
@@ -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();
} }
-9
View File
@@ -1,9 +0,0 @@
import QtQuick
import Quickshell
import qs.Modules.Greetd
Scope {
id: root
GreeterSurface {}
}
+2
View File
@@ -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";
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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 {
+1 -1
View File
@@ -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
}, },
{ {
@@ -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)}%)`;
+6 -2
View File
@@ -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
+14 -20
View File
@@ -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
+4 -1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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 {
+1
View File
@@ -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)
+1
View File
@@ -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",
-20
View File
@@ -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;
}
-162
View File
@@ -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();
}
}
-7
View File
@@ -1,7 +0,0 @@
# Embedded Greetd module
TODO: remove when dank-greeter is available in the Arch extra repos.
The greeter moved to [dank-greeter](https://github.com/duxlabs-dev/dux-greeter). This embedded copy exists only for archinstall compatibility: the `niri - Dux Shell` archinstall profile writes an `/etc/greetd/config.toml` that launches `Modules/Greetd/assets/dms-greeter` from the packaged DMS tree, and dank-greeter is not installable from the official Arch repos yet.
Do not extend this module. New greeter work goes to dank-greeter. Installing greetd-dms-greeter-bin from the AUR and running `dms-greeter sync` migrates a system off this embedded copy.
@@ -1,506 +0,0 @@
#!/bin/bash
set -e
COMPOSITOR=""
COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() {
cat << EOF
dms-greeter - Dux Shell greeter launcher
Usage: dms-greeter --command COMPOSITOR [OPTIONS]
Required:
--command COMPOSITOR Compositor to use (niri, hyprland, sway, scroll, miracle, mango, or labwc)
Options:
-C, --config PATH Custom compositor config file
-p, --path PATH DMS path (config name or absolute path)
(default: dms-greeter)
--cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message
Examples:
dms-greeter --command niri
dms-greeter --command hyprland -C /etc/greetd/custom-hypr.lua
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango
dms-greeter --command labwc
EOF
}
require_command() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Error: required command '$cmd' was not found in PATH" >&2
exit 1
fi
}
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
is_hyprland_lua_config() {
local config_file="$1"
case "$config_file" in
*.lua)
return 0
;;
esac
if [[ -f "$config_file" ]] && grep -qE '(^|[^[:alnum:]_])hl\.' "$config_file"; then
return 0
fi
return 1
}
clear_vt() {
local clear_sequence=$'\033[2J\033[H\033[3J\033[?25l'
# Clear retained console text on the controlling VT.
if printf '%s' "$clear_sequence" >/dev/tty 2>/dev/null; then
return
fi
if [[ -t 1 ]]; then
printf '%s' "$clear_sequence"
fi
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
clear_vt
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info 2>/dev/null) 2>&1
fi
# Preserve diagnostics on systems without journald.
exec "$@" >>"$CACHE_DIR/$log_tag.log" 2>&1
}
while [[ $# -gt 0 ]]; do
case $1 in
--command)
COMPOSITOR="$2"
shift 2
;;
-C|--config)
COMPOSITOR_CONFIG="$2"
shift 2
;;
-p|--path)
DMS_PATH="$2"
shift 2
;;
--cache-dir)
CACHE_DIR="$2"
shift 2
;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1" >&2
show_help
exit 1
;;
esac
done
if [[ -z "$COMPOSITOR" ]]; then
echo "Error: --command COMPOSITOR is required" >&2
show_help
exit 1
fi
locate_dms_config() {
local config_name="$1"
local search_paths=()
local config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
search_paths+=("$config_home/quickshell/$config_name")
search_paths+=("/usr/share/quickshell/$config_name")
local config_dirs="${XDG_CONFIG_DIRS:-/etc/xdg}"
IFS=':' read -ra dirs <<< "$config_dirs"
for dir in "${dirs[@]}"; do
if [[ -n "$dir" ]]; then
search_paths+=("$dir/quickshell/$config_name")
fi
done
for path in "${search_paths[@]}"; do
if [[ -f "$path/shell.qml" ]]; then
echo "$path"
return 0
fi
done
return 1
}
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1
if [[ ! -d "$CACHE_DIR" ]]; then
echo "Error: cache directory '$CACHE_DIR' does not exist." >&2
echo " Run 'dms greeter sync' to initialize it, or pass --cache-dir to an existing directory." >&2
exit 1
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR"
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Fallback runtime dir for systems without logind/pam_rundir (e.g. Void+seatd),
# where Wayland compositors abort with "RuntimeDirNotSet". Guarded so logind
# systems keep their own.
if [[ -z "${XDG_RUNTIME_DIR:-}" || ! -d "${XDG_RUNTIME_DIR:-}" ]]; then
export XDG_RUNTIME_DIR="$CACHE_DIR/run"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
fi
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
# Export the user's configured cursor (from synced settings.json) for the
# compositor and greeter shell, but only if that theme is installed system-wide.
apply_user_cursor() {
command -v jq >/dev/null 2>&1 || return 0
local settings="$DMS_GREET_CFG_DIR/settings.json"
[[ -f "$settings" ]] || return 0
local theme size
theme=$(jq -r '.cursorSettings.theme // empty' "$settings" 2>/dev/null)
size=$(jq -r '.cursorSettings.size // empty' "$settings" 2>/dev/null)
[[ -n "$theme" && "$theme" != "System Default" ]] || return 0
local dirs=()
IFS=':' read -ra data_dirs <<< "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
local d
for d in "${data_dirs[@]}"; do
[[ -n "$d" ]] && dirs+=("$d/icons")
done
dirs+=(/run/current-system/sw/share/icons /usr/share/icons /usr/local/share/icons)
for d in "${dirs[@]}"; do
if [[ -d "$d/$theme/cursors" ]]; then
export XCURSOR_PATH="$d${XCURSOR_PATH:+:$XCURSOR_PATH}"
export XCURSOR_THEME="$theme"
[[ -n "$size" ]] && export XCURSOR_SIZE="$size"
return 0
fi
done
}
apply_user_cursor
if command -v qs >/dev/null 2>&1; then
QS_BIN="qs"
elif command -v quickshell >/dev/null 2>&1; then
QS_BIN="quickshell"
else
echo "Error: neither 'qs' nor 'quickshell' was found in PATH" >&2
exit 1
fi
QS_CMD="$QS_BIN"
if [[ "$DMS_PATH" == /* ]]; then
QS_CMD="$QS_BIN -p $DMS_PATH"
else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
exit 1
fi
fi
case "$COMPOSITOR" in
niri)
require_command "niri"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
# base/default config
cat > "$TEMP_CONFIG" << NIRI_EOF
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
fi
# Append includes if override_files exist
OVERRIDE_FILES=(
/usr/share/greetd/niri_overrides.kdl # for building into a system image
/etc/greetd/niri_overrides.kdl # for local overrides
)
for override_file in "${OVERRIDE_FILES[@]}"; do
if [[ -e "$override_file" ]]; then
# TODO: maybe move to optional=true when Niri Next drops, so we can avoid these checks ;-)
cat >> "$TEMP_CONFIG" << NIRI_EOF
include "$override_file"
NIRI_EOF
fi
done
# Append spawn command
cat >> "$TEMP_CONFIG" << NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
;;
hyprland)
if ! command -v start-hyprland >/dev/null 2>&1 && ! command -v Hyprland >/dev/null 2>&1; then
echo "Error: neither 'start-hyprland' nor 'Hyprland' was found in PATH" >&2
exit 1
fi
if [[ -n "$COMPOSITOR_CONFIG" ]] && is_hyprland_lua_config "$COMPOSITOR_CONFIG"; then
TEMP_CONFIG=$(mktemp --suffix=.lua)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
hl.on("hyprland.start", function()
hl.exec_cmd("sh -c \"$QS_CMD; hyprctl dispatch 'hl.dsp.exit()'\"")
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp --suffix=.lua)
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
hl.env("DMS_RUN_GREETER", "1")
hl.config({
misc = {
disable_hyprland_logo = true,
},
})
hl.on("hyprland.start", function()
hl.exec_cmd("sh -c \"$QS_CMD; hyprctl dispatch 'hl.dsp.exit()'\"")
end)
HYPRLAND_LUA_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
# hyprlang stays usable until 0.57 ships; legacy dispatch is the only form a .conf session accepts
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
if command -v start-hyprland >/dev/null 2>&1; then
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
else
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
fi
;;
sway)
require_command "sway"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF
exec "$QS_CMD; swaymsg exit"
SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;;
scroll)
require_command "scroll"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SCROLL_EOF
exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SCROLL_EOF
exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
;;
miracle|miracle-wm)
require_command "miracle-wm"
if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
else
TEMP_CONFIG=$(mktemp)
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << MIRACLE_EOF
exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
;;
labwc)
require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else
exec_compositor "labwc" labwc --session "$QS_CMD"
fi
;;
mango|mangowc)
require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
else
# Without -c mango falls back to /etc/mango/config.conf, whose spawn/quit binds work at the login screen.
TEMP_CONFIG=$(mktemp)
exec_compositor "mango" mango -c "$TEMP_CONFIG" -s "$QS_CMD && mmsg dispatch quit"
fi
;;
*)
echo "Error: Unsupported compositor: $COMPOSITOR" >&2
echo "Supported compositors: niri, hyprland, sway, scroll, miracle, mango, labwc" >&2
exit 1
;;
esac
@@ -1,8 +0,0 @@
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
hl.env("DMS_RUN_GREETER", "1")
hl.on("hyprland.start", function()
hl.exec_cmd("sh -c \"qs -p _DMS_PATH_; hyprctl dispatch 'hl.dsp.exit()'\"")
end)
@@ -1,19 +0,0 @@
hotkey-overlay {
skip-at-startup
}
environment {
DMS_RUN_GREETER "1"
}
spawn-at-startup "sh" "-c" "qs -p _DMS_PATH_; niri msg action quit --skip-confirmation"
gestures {
hot-corners {
off
}
}
layout {
background-color "#000000"
}
@@ -1,10 +0,0 @@
#!/bin/sh
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export EGL_PLATFORM=gbm
if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- -c /etc/greetd/dms-hypr.lua
else
exec Hyprland -c /etc/greetd/dms-hypr.lua
fi
@@ -1,7 +0,0 @@
#!/bin/sh
export XDG_SESSION_TYPE=wayland
export QT_QPA_PLATFORM=wayland
export EGL_PLATFORM=gbm
exec niri -c /etc/greetd/dms-niri.kdl
+8
View File
@@ -220,6 +220,8 @@ Scope {
function onSessionUnlocked() { function onSessionUnlocked() {
customLockerSpawned = false; customLockerSpawned = false;
if (AdminPolicy.disableLogindUnlock)
return;
if (pendingLock) { if (pendingLock) {
pendingLock = false; pendingLock = false;
lockInitiatedLocally = false; lockInitiatedLocally = false;
@@ -412,14 +414,20 @@ Scope {
} }
function unlock() { function unlock() {
if (AdminPolicy.disableLockIpcUnlock)
return;
root.unlock(); root.unlock();
} }
function forceReset() { function forceReset() {
if (AdminPolicy.disableLockIpcUnlock)
return;
root.forceReset(); root.forceReset();
} }
function demo() { function demo() {
if (AdminPolicy.disableLockIpcUnlock)
return;
demoWindow.showDemo(); demoWindow.showDemo();
} }
+32 -8
View File
@@ -10,6 +10,8 @@ import qs.Services
Scope { Scope {
id: root id: root
readonly property var log: Log.scoped("Pam")
property bool lockSecured: false property bool lockSecured: false
property bool unlockInProgress: false property bool unlockInProgress: false
@@ -96,6 +98,8 @@ Scope {
if (!root.u2fSuppressedByPrimaryPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) { if (!root.u2fSuppressedByPrimaryPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
u2f.startForSecondFactor(); u2f.startForSecondFactor();
} else { } else {
if (AdminPolicy.disableCustomLockPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && !u2fConfigWatcher.loaded)
log.warn("/etc/pam.d/dankshell-u2f is missing, unlocking without the security key");
completeUnlock(); completeUnlock();
} }
} }
@@ -113,8 +117,8 @@ Scope {
} }
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
readonly property bool fprintSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineFprint) readonly property bool fprintSuppressedByPrimaryPam: !AdminPolicy.disableCustomLockPam && (SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineFprint))
readonly property bool u2fSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineU2f) readonly property bool u2fSuppressedByPrimaryPam: !AdminPolicy.disableCustomLockPam && (SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineU2f))
readonly property bool customU2fPamActive: SettingsData.lockU2fPamPath !== "" && customU2fPamWatcher.loaded readonly property bool customU2fPamActive: SettingsData.lockU2fPamPath !== "" && customU2fPamWatcher.loaded
FileView { FileView {
@@ -139,6 +143,13 @@ Scope {
printErrors: false printErrors: false
} }
FileView {
id: fprintConfigWatcher
path: AdminPolicy.disableCustomLockPam ? "/etc/pam.d/dankshell-fprint" : ""
printErrors: false
}
FileView { FileView {
id: customU2fPamWatcher id: customU2fPamWatcher
@@ -169,7 +180,7 @@ Scope {
} }
function ensureUserPamConfig(): void { function ensureUserPamConfig(): void {
if (SettingsData.lockPamExternallyManaged || resolveUserPam.running) if (AdminPolicy.disableCustomLockPam || SettingsData.lockPamExternallyManaged || resolveUserPam.running)
return; return;
resolveUserPam.running = true; resolveUserPam.running = true;
} }
@@ -180,6 +191,8 @@ Scope {
id: passwd id: passwd
config: { config: {
if (AdminPolicy.disableCustomLockPam)
return dankshellConfigWatcher.loaded ? "dankshell" : "login";
if (root.customPamActive) if (root.customPamActive)
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1); return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
if (SettingsData.lockPamExternallyManaged) if (SettingsData.lockPamExternallyManaged)
@@ -191,6 +204,8 @@ Scope {
return "login"; return "login";
} }
configDirectory: { configDirectory: {
if (AdminPolicy.disableCustomLockPam)
return "/etc/pam.d";
if (root.customPamActive) { if (root.customPamActive) {
const idx = SettingsData.lockPamPath.lastIndexOf("/"); const idx = SettingsData.lockPamPath.lastIndexOf("/");
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/"; return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
@@ -299,14 +314,15 @@ Scope {
PamContext { PamContext {
id: fprint id: fprint
readonly property bool available: SettingsData.lockFingerprintReady readonly property bool available: SettingsData.lockFingerprintReady && (!AdminPolicy.disableCustomLockPam || fprintConfigWatcher.loaded)
property int tries: 0 property int tries: 0
property int errorTries: 0 property int errorTries: 0
property double attemptStartedAt: 0 property double attemptStartedAt: 0
property bool completedDuringStart: false property bool completedDuringStart: false
readonly property int maxErrorTries: 200 readonly property int maxErrorTries: 200
readonly property int daemonIdleExitMs: 30000 readonly property int daemonIdleExitMs: 30000
// sessionTimeoutMs mirrors `timeout=` in assets/pam/fprint // sessionTimeoutMs mirrors `timeout=` in assets/pam/fprint; under the
// policy, /etc/pam.d/dankshell-fprint must carry the same value
readonly property int sessionTimeoutMs: 90000 readonly property int sessionTimeoutMs: 90000
readonly property int verifyStartSlackMs: 10000 readonly property int verifyStartSlackMs: 10000
readonly property bool retrying: errorRetry.running readonly property bool retrying: errorRetry.running
@@ -401,8 +417,8 @@ Scope {
Qt.callLater(checkAvail); Qt.callLater(checkAvail);
} }
config: "fprint" config: AdminPolicy.disableCustomLockPam ? "dankshell-fprint" : "fprint"
configDirectory: Quickshell.shellDir + "/assets/pam" configDirectory: AdminPolicy.disableCustomLockPam ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onPamMessage: { onPamMessage: {
if (attemptStartedAt === 0) if (attemptStartedAt === 0)
@@ -446,7 +462,7 @@ Scope {
PamContext { PamContext {
id: u2f id: u2f
property bool available: SettingsData.lockU2fReady property bool available: SettingsData.lockU2fReady && (!AdminPolicy.disableCustomLockPam || u2fConfigWatcher.loaded)
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByPrimaryPam) { if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByPrimaryPam) {
@@ -483,11 +499,15 @@ Scope {
} }
config: { config: {
if (AdminPolicy.disableCustomLockPam)
return "dankshell-u2f";
if (root.customU2fPamActive) if (root.customU2fPamActive)
return SettingsData.lockU2fPamPath.slice(SettingsData.lockU2fPamPath.lastIndexOf("/") + 1); return SettingsData.lockU2fPamPath.slice(SettingsData.lockU2fPamPath.lastIndexOf("/") + 1);
return u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"; return u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f";
} }
configDirectory: { configDirectory: {
if (AdminPolicy.disableCustomLockPam)
return "/etc/pam.d";
if (root.customU2fPamActive) { if (root.customU2fPamActive) {
const idx = SettingsData.lockU2fPamPath.lastIndexOf("/"); const idx = SettingsData.lockU2fPamPath.lastIndexOf("/");
return idx > 0 ? SettingsData.lockU2fPamPath.slice(0, idx) : "/"; return idx > 0 ? SettingsData.lockU2fPamPath.slice(0, idx) : "/";
@@ -694,6 +714,8 @@ Scope {
} }
function onLockPamExternallyManagedChanged(): void { function onLockPamExternallyManagedChanged(): void {
if (AdminPolicy.disableCustomLockPam)
return;
root.resetAuthFlows(); root.resetAuthFlows();
if (!SettingsData.lockPamExternallyManaged) if (!SettingsData.lockPamExternallyManaged)
root.ensureUserPamConfig(); root.ensureUserPamConfig();
@@ -702,6 +724,8 @@ Scope {
} }
function onLockU2fPamPathChanged(): void { function onLockU2fPamPathChanged(): void {
if (AdminPolicy.disableCustomLockPam)
return;
u2f.abort(); u2f.abort();
u2f.checkAvail(); u2f.checkAvail();
} }
+1 -1
View File
@@ -36,7 +36,7 @@ Item {
onLoaded: { onLoaded: {
item.errorOccurred.connect((error, errorString) => { item.errorOccurred.connect((error, errorString) => {
log.warn("playback error:", errorString); log.warn("playback error:", errorString);
ToastService.showError(I18n.tr("Video screensaver"), I18n.tr("Playback error: ") + errorString); ToastService.showError(I18n.tr("Video screensaver"), I18n.tr("Playback error: %1", "%1 is the player error text").arg(errorString));
root.dismiss(); root.dismiss();
}); });
if (root.videoSource) { if (root.videoSource) {
@@ -55,7 +55,7 @@ Rectangle {
function saveDisplayImage() { function saveDisplayImage() {
const data = root.notificationData; const data = root.notificationData;
if (!root.persistImage || !data?.hasDisplayImage || data.persistedImagePath) if (!root.persistImage || !data?.notification || !data.hasDisplayImage || data.persistedImagePath)
return; return;
const image = data.image || ""; const image = data.image || "";
if (!image.startsWith("image://qsimage/") && !NotificationService.notificationIconFromImage(image).startsWith("/")) if (!image.startsWith("image://qsimage/") && !NotificationService.notificationIconFromImage(image).startsWith("/"))
@@ -61,6 +61,7 @@ PanelWindow {
readonly property bool cardShown: presenting && (presentationProgress > 0 || !exiting) readonly property bool cardShown: presenting && (presentationProgress > 0 || !exiting)
readonly property bool layoutPinned: !exiting && (hovered || contextMenuActive || swipeActive) readonly property bool layoutPinned: !exiting && (hovered || contextMenuActive || swipeActive)
property bool exiting: false property bool exiting: false
property var exitSnapshot: null
property bool exitStarted: false property bool exitStarted: false
property bool collapseChromeOnExit: false property bool collapseChromeOnExit: false
property bool _isDestroying: false property bool _isDestroying: false
@@ -124,6 +125,7 @@ PanelWindow {
} }
enterAnimation.stop(); enterAnimation.stop();
enterDelay.stop(); enterDelay.stop();
exitSnapshot = snapshotNotification(notificationData);
exiting = true; exiting = true;
if (NotificationService.removeFromVisibleNotifications) if (NotificationService.removeFromVisibleNotifications)
NotificationService.removeFromVisibleNotifications(win.notificationData); NotificationService.removeFromVisibleNotifications(win.notificationData);
@@ -172,6 +174,29 @@ PanelWindow {
win.exitFinished(); win.exitFinished();
} }
// The wrapper is destroyed as soon as its notification closes, which would blank the card mid-exit.
function snapshotNotification(data) {
if (!data)
return null;
return {
"appName": data.appName,
"appIcon": data.appIcon,
"desktopEntry": data.desktopEntry,
"summary": data.summary,
"body": data.body,
"htmlBody": data.htmlBody,
"image": data.image,
"hasDisplayImage": data.hasDisplayImage,
"displayImage": data.displayImage,
"persistedImagePath": data.persistedImagePath,
"timeStr": data.timeStr,
"urgency": data.urgency,
"actions": Array.from(data.actions || [], action => ({
"text": action.text
}))
};
}
function closeTransientUi() { function closeTransientUi() {
transientSurfaces.closeAll(); transientSurfaces.closeAll();
popupContextMenuLoader.active = false; popupContextMenuLoader.active = false;
@@ -719,7 +744,7 @@ PanelWindow {
chipColor: Theme.notificationChipSurface chipColor: Theme.notificationChipSurface
width: parent.width width: parent.width
height: win.inlineHeightAnimating ? Math.min(targetHeight, cardSurface.height - win.timeoutRailClearance) : targetHeight height: win.inlineHeightAnimating ? Math.min(targetHeight, cardSurface.height - win.timeoutRailClearance) : targetHeight
notificationData: win.notificationData notificationData: win.exitSnapshot ?? win.notificationData
descriptionExpanded: win.descriptionExpanded descriptionExpanded: win.descriptionExpanded
privacyMode: SettingsData.notificationPopupPrivacyMode privacyMode: SettingsData.notificationPopupPrivacyMode
bodyInvokesAction: win.bodyClickInvokesAction bodyInvokesAction: win.bodyClickInvokesAction
@@ -557,6 +557,13 @@ QtObject {
} }
Component.onCompleted: _sync(NotificationService.visibleNotifications) Component.onCompleted: _sync(NotificationService.visibleNotifications)
Component.onDestruction: {
for (const p of popupWindows)
p?.destroy();
const screenName = manager.modelData?.name || "";
if (screenName)
ConnectedModeState.releaseSurface(screenName, "notification", "notification:" + screenName);
}
onPopupSpacingChanged: _repositionAll() onPopupSpacingChanged: _repositionAll()
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync() onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
onCloseGapNotificationsChanged: _scheduleNotificationChromeSync() onCloseGapNotificationsChanged: _scheduleNotificationChromeSync()
@@ -204,7 +204,7 @@ Item {
id: errorText id: errorText
visible: pluginService && !root.hasPermission visible: pluginService && !root.hasPermission
anchors.fill: parent anchors.fill: parent
text: I18n.tr("This plugin does not have 'settings_write' permission.\n\nAdd \"permissions\": [\"settings_read\", \"settings_write\"] to plugin.json") text: I18n.tr("This plugin does not have '%1' permission.\n\nAdd %2 to %3", "plugin settings write warning, %1 is a permission name, %2 a manifest snippet, %3 the manifest file").arg("settings_write").arg('"permissions": ["settings_read", "settings_write"]').arg("plugin.json")
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -209,7 +209,7 @@ Popup {
contentItem: ConfirmDialogContent { contentItem: ConfirmDialogContent {
id: confirmationContent id: confirmationContent
confirmTitle: confirmation.forceKill ? I18n.tr("Force Kill (SIGKILL)") : I18n.tr("Kill Process") confirmTitle: confirmation.forceKill ? I18n.tr("Force Kill (SIGKILL)") : I18n.tr("Kill Process")
confirmMessage: (confirmation.process?.command ?? "") + " (" + I18n.tr("PID") + " " + (confirmation.process?.pid ?? 0) + ")" confirmMessage: (confirmation.process?.command ?? "") + " (" + I18n.tr("PID %1", "%1 is a process id").arg(confirmation.process?.pid ?? 0) + ")"
confirmButtonText: I18n.tr("Kill Process") confirmButtonText: I18n.tr("Kill Process")
confirmButtonColor: Theme.error confirmButtonColor: Theme.error
Keys.onPressed: event => { Keys.onPressed: event => {
@@ -15,7 +15,7 @@ RowLayout {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
text: (DgopService.hostname || "localhost") + " · " + (DgopService.distribution || "Linux") + " · " + I18n.tr("Uptime") + ": " + (DgopService.shortUptime ? DgopService.shortUptime.slice(2).trim() : "--") + " · " + DgopService.processCount + " " + I18n.tr("procs", "short for processes") text: (DgopService.hostname || "localhost") + " · " + (DgopService.distribution || "Linux") + " · " + I18n.tr("Uptime") + ": " + (DgopService.shortUptime ? DgopService.shortUptime.slice(2).trim() : "--") + " · " + I18n.tr("%1 procs", "short for processes, %1 is a count").arg(DgopService.processCount)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.onSurfaceVariant color: Theme.onSurfaceVariant
elide: Text.ElideRight elide: Text.ElideRight
@@ -25,8 +25,8 @@ RowLayout {
model: DgopService.availableGpus.filter(gpu => (SessionData.enabledGpuPciIds || []).includes(gpu.pciId) && gpu.temperature > 0) model: DgopService.availableGpus.filter(gpu => (SessionData.enabledGpuPciIds || []).includes(gpu.pciId) && gpu.temperature > 0)
NumericText { NumericText {
required property var modelData required property var modelData
text: I18n.tr("GPU") + " " + modelData.temperature.toFixed(0) + "°C" text: I18n.tr("GPU %1", "%1 is a temperature").arg(modelData.temperature.toFixed(0) + "°C")
reserveText: I18n.tr("GPU") + " 100°C" reserveText: I18n.tr("GPU %1", "%1 is a temperature").arg("100°C")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: modelData.temperature > 85 ? Theme.error : Theme.onSurfaceVariant color: modelData.temperature > 85 ? Theme.error : Theme.onSurfaceVariant
} }
+1 -1
View File
@@ -474,7 +474,7 @@ Item {
SettingsRow { SettingsRow {
body: StyledText { body: StyledText {
width: parent.width width: parent.width
text: I18n.tr("These add entries to the XDG autostart directory (~/.config/autostart/*.desktop)") text: I18n.tr("These add entries to the XDG autostart directory (%1)", "autostart note, %1 is a path").arg("~/.config/autostart/*.desktop")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -167,7 +167,7 @@ Item {
settingKey: "createUserGreeter" settingKey: "createUserGreeter"
tags: ["user", "greeter", "login", "sync"] tags: ["user", "greeter", "login", "sync"]
text: I18n.tr("Allow greeter login access") text: I18n.tr("Allow greeter login access")
description: I18n.tr("Adds them to the %1 group so they can run dms-greeter sync --profile", "create user greeter toggle description, %1 is a group name").arg(UsersService.greeterGroup) description: I18n.tr("Adds them to the %1 group so they can run %2", "create user greeter toggle description, %1 is a group name, %2 a command").arg(UsersService.greeterGroup).arg("dms-greeter sync --profile")
checked: root.pendingGreeter checked: root.pendingGreeter
onToggled: checked => root.pendingGreeter = checked onToggled: checked => root.pendingGreeter = checked
} }
@@ -766,7 +766,7 @@ Singleton {
if (!match) { if (!match) {
profilesLoading = false; profilesLoading = false;
manualActivation = false; manualActivation = false;
profileError(I18n.tr("Profile not found in monitors.json")); profileError(I18n.tr("Profile not found in %1", "display profile error, %1 is the profiles file").arg("monitors.json"));
return; return;
} }
applyConfigEntry(match.entry, profileId, match.entry.name || profileId, true); applyConfigEntry(match.entry, profileId, match.entry.name || profileId, true);
@@ -1150,7 +1150,7 @@ Singleton {
} }
function showHyprlandReadOnlyWarning() { function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "display-config"); ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using %1. Run %2 to migrate before changing these settings.", "hyprland legacy config warning, %1 is the config file, %2 a command").arg("hyprland.conf").arg("dms setup"), "dms setup", "display-config");
} }
function buildOutputsMap() { function buildOutputsMap() {
@@ -24,7 +24,7 @@ DModal {
const kind = p.trcKind; const kind = p.trcKind;
if (kind === "gamma") if (kind === "gamma")
return I18n.tr("Gamma", "Gamma") + " " + (p.trcGamma ?? 0).toFixed(2); return I18n.tr("Gamma %1", "%1 is a gamma value").arg((p.trcGamma ?? 0).toFixed(2));
if (kind === "table") if (kind === "table")
return I18n.tr("Table", "Tone curve stored as a lookup table") + " · " + p.trcEntries; return I18n.tr("Table", "Tone curve stored as a lookup table") + " · " + p.trcEntries;
if (kind === "identity") if (kind === "identity")
+35 -20
View File
@@ -12,6 +12,7 @@ Column {
property var parentModal: null property var parentModal: null
property string editingDockId: "" property string editingDockId: ""
property string renameDraft: "" property string renameDraft: ""
readonly property var dockPages: ["dock_general", "dock_widgets", "dock_appearance"].map(id => SettingsTabs.page(id)).filter(page => page)
function finishRename(value) { function finishRename(value) {
const id = editingDockId; const id = editingDockId;
@@ -44,11 +45,10 @@ Column {
} }
SettingsCard { SettingsCard {
width: parent.width
iconName: "dock_to_bottom" iconName: "dock_to_bottom"
title: I18n.tr("Docks", "noun plural, settings card title listing configured docks") title: I18n.tr("Docks", "noun plural, settings card title listing configured docks")
settingKey: "dockConfiguration" settingKey: "dockConfiguration"
tags: ["dock", "configuration", "add", "remove", "name", "show", "enable"] tags: ["dock", "configuration", "add", "remove", "show", "enable"]
headerActions: DButton { headerActions: DButton {
text: I18n.tr("Add", "verb, button that adds a new item to a list") text: I18n.tr("Add", "verb, button that adds a new item to a list")
iconName: "add" iconName: "add"
@@ -75,11 +75,18 @@ Column {
onDeleteRequested: root.removeDock(modelData.id) onDeleteRequested: root.removeDock(modelData.id)
} }
} }
}
SettingsCard {
iconName: "dock_to_bottom"
title: dock.config?.name ?? ""
settingKey: "dockPlacement"
tags: ["dock", "layout", "placement", "position", "edge", "name", "rename"]
visible: dock.hasConfig
SettingsRow { SettingsRow {
title: I18n.tr("Name") title: I18n.tr("Name")
subtitle: root.editingDockId ? "" : dock.config?.name ?? "" subtitle: root.editingDockId ? "" : dock.config?.name ?? ""
visible: dock.hasConfig
DActionButton { DActionButton {
iconName: root.editingDockId ? "check" : "edit" iconName: root.editingDockId ? "check" : "edit"
@@ -120,25 +127,33 @@ Column {
} }
} }
} }
}
SettingsCard { SettingsRow {
width: parent.width title: I18n.tr("Layout", "noun, settings section title for arrangement options")
visible: dock.hasConfig
iconName: "dock_to_bottom"
title: I18n.tr("Layout")
settingKey: "dockPlacement"
tags: ["dock", "layout", "placement", "position", "edge"]
SettingsLayoutPicker { body: SettingsLayoutPicker {
edgePlacement: true edgePlacement: true
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({ choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
key: String(position), key: String(position),
label: dock.positionLabel(position), label: dock.positionLabel(position),
enabled: dock.positionChoices.includes(position) enabled: dock.positionChoices.includes(position)
})) }))
selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom) selectedKey: String(dock.config?.position ?? SettingsData.Position.Bottom)
onSelected: key => dock.setEnabled(true, Number(key)) onSelected: key => dock.setEnabled(true, Number(key))
}
}
Repeater {
model: root.dockPages
delegate: SettingsNavRow {
required property var modelData
iconName: modelData.icon
title: modelData.text
hint: modelData.hint ?? ""
onClicked: keyboard => root.parentModal?.navigateTo(modelData.id, keyboard)
}
} }
} }
} }
@@ -25,7 +25,7 @@ Item {
case "missing_reader": case "missing_reader":
return I18n.tr("No fingerprint reader detected", "fingerprint setting status"); return I18n.tr("No fingerprint reader detected", "fingerprint setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM.", "greeter fingerprint login setting"); return I18n.tr("Not available - install %1 and %2, or configure %3 PAM", "greeter fingerprint login setting, %1 and %2 are package names, %3 is greetd").arg("fprintd").arg("pam_fprintd").arg("greetd");
default: default:
return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status"); return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status");
} }
@@ -43,7 +43,7 @@ Item {
case "missing_key_registration": case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status"); return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM.", "greeter security key login setting"); return I18n.tr("Not available - install or configure %1, or configure %2 PAM", "greeter security key login setting, %1 is a PAM module, %2 is greetd").arg("pam_u2f").arg("greetd");
default: default:
return I18n.tr("Security-key availability could not be confirmed", "security key setting status"); return I18n.tr("Security-key availability could not be confirmed", "security key setting status");
} }
@@ -70,7 +70,7 @@ Item {
settingKey: "greeterPamExternallyManaged" settingKey: "greeterPamExternallyManaged"
tags: ["greeter", "pam", "managed", "external", "greetd", "auth"] tags: ["greeter", "pam", "managed", "external", "greetd", "auth"]
text: I18n.tr("Use system PAM authentication", "system PAM policy toggle") text: I18n.tr("Use system PAM authentication", "system PAM policy toggle")
description: I18n.tr("DMS removes its managed block from /etc/pam.d/greetd and stops write services", "greeter system PAM toggle description") description: I18n.tr("DMS removes its managed block from %1 and stops write services", "greeter system PAM toggle description, %1 is a PAM config path").arg("/etc/pam.d/greetd")
checked: SettingsData.greeterPamExternallyManaged checked: SettingsData.greeterPamExternallyManaged
onToggled: checked => SettingsData.set("greeterPamExternallyManaged", checked) onToggled: checked => SettingsData.set("greeterPamExternallyManaged", checked)
} }
+7 -25
View File
@@ -62,13 +62,11 @@ Item {
property string greeterStatusStderr: "" property string greeterStatusStderr: ""
readonly property bool greeterBinaryExists: GreeterService.binaryExists readonly property bool greeterBinaryExists: GreeterService.binaryExists
readonly property bool greeterEnabled: GreeterService.enabled readonly property bool greeterEnabled: GreeterService.enabled
property bool embeddedGreeterConfigured: false
readonly property bool embeddedGreeterOnly: embeddedGreeterConfigured && !greeterBinaryExists
readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning readonly property bool busy: greeterSyncRunning || greeterInstallActionRunning
readonly property string greeterState: { readonly property string greeterState: {
if (!greeterBinaryExists) if (!greeterBinaryExists)
return embeddedGreeterOnly ? "embedded" : "missing"; return "missing";
if (!greeterEnabled) if (!greeterEnabled)
return "inactive"; return "inactive";
switch (GreeterService.slotState) { switch (GreeterService.slotState) {
@@ -103,8 +101,6 @@ Item {
return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter"); return I18n.tr("Not active", "greeter status, greetd does not run the DMS greeter");
case "unlinked": case "unlinked":
return I18n.tr("Not linked", "greeter status, no greeter slot for this user"); return I18n.tr("Not linked", "greeter status, no greeter slot for this user");
case "embedded":
return I18n.tr("Bundled greeter active", "greeter status");
} }
return I18n.tr("Not installed", "greeter status"); return I18n.tr("Not installed", "greeter status");
} }
@@ -118,10 +114,8 @@ Item {
return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status"); return I18n.tr("greetd is not using the DMS greeter yet.", "greeter status");
case "unlinked": case "unlinked":
return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status"); return GreeterService.profileSyncSufficient ? I18n.tr("Link your DMS settings so the login screen follows them.", "greeter status") : I18n.tr("Link your DMS settings so the login screen follows them. Administrator access is required.", "greeter status");
case "embedded":
return I18n.tr("The greeter bundled with DMS is active (archinstall setup). It keeps working as is, but syncing theme and settings needs the standalone greeter. Install greetd-dms-greeter-bin from the AUR, then run Sync to migrate the login screen.", "embedded greeter status");
} }
return I18n.tr("dms-greeter is not installed. Install the dms-greeter package to manage the greeter.", "greeter status placeholder"); return I18n.tr("%1 is not installed. Install the package to manage the greeter.", "greeter status placeholder, %1 is the dms-greeter binary").arg("dms-greeter");
} }
readonly property string greeterStateAction: { readonly property string greeterStateAction: {
switch (greeterState) { switch (greeterState) {
@@ -149,7 +143,6 @@ Item {
function checkGreeterInstallState() { function checkGreeterInstallState() {
GreeterService.refresh(); GreeterService.refresh();
embeddedGreeterCheckProcess.running = true;
} }
function runGreeterStatus() { function runGreeterStatus() {
@@ -161,7 +154,7 @@ Item {
} }
function runGreeterInstallAction() { function runGreeterInstallAction() {
greeterStatusText = I18n.tr("Opening terminal: ") + I18n.tr("Activate") + "..."; greeterStatusText = I18n.tr("Opening terminal: %1", "%1 is the action name").arg(I18n.tr("Activate")) + "...";
greeterInstallActionRunning = true; greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true; greeterInstallActionProcess.running = true;
} }
@@ -205,17 +198,6 @@ Item {
} }
} }
Process {
id: embeddedGreeterCheckProcess
// archinstall's DMS profile points greetd at this launcher inside the packaged DMS tree
command: ["sh", "-c", "grep -q 'Modules/Greetd/assets/dms-greeter' /etc/greetd/config.toml 2>/dev/null"]
running: false
onExited: exitCode => {
root.embeddedGreeterConfigured = (exitCode === 0);
}
}
Process { Process {
id: greeterStatusProcess id: greeterStatusProcess
command: ["dms-greeter", "status"] command: ["dms-greeter", "status"]
@@ -241,7 +223,7 @@ Item {
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err; root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
return; return;
} }
var failure = I18n.tr("Failed to run 'dms-greeter status'. Ensure the dms-greeter package is installed.", "greeter status error") + " (exit " + exitCode + ")"; var failure = I18n.tr("Failed to run '%1'. Ensure the %2 package is installed.", "greeter status error, %1 is a command, %2 a package name").arg("dms-greeter status").arg("dms-greeter") + " " + I18n.tr("(exit %1)", "process exit code").arg(exitCode);
if (out !== "") if (out !== "")
failure = failure + "\n\n" + out; failure = failure + "\n\n" + out;
if (err !== "") if (err !== "")
@@ -267,7 +249,7 @@ Item {
root.greeterInstallActionRunning = false; root.greeterInstallActionRunning = false;
root.checkGreeterInstallState(); root.checkGreeterInstallState();
if (exitCode !== 0) { if (exitCode !== 0) {
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")"; root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " " + I18n.tr("(exit %1)", "process exit code").arg(exitCode);
return; return;
} }
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled."); root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
@@ -479,7 +461,7 @@ Item {
SettingsRow { SettingsRow {
body: StyledText { body: StyledText {
text: I18n.tr("Requires greetd, dms-greeter, and your user in the greeter group (plus fprintd/pam_fprintd for fingerprint, pam_u2f for security keys).") text: I18n.tr("Requires %1, %2, and your user in the %3 group (plus %4 for fingerprint, %5 for security keys).", "greeter requirements note, %1 and %2 are package names, %3 a group name, %4 and %5 PAM modules").arg("greetd").arg("dms-greeter").arg("greeter").arg("fprintd/pam_fprintd").arg("pam_u2f")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
@@ -490,7 +472,7 @@ Item {
SettingsRow { SettingsRow {
body: StyledText { body: StyledText {
text: I18n.tr("Installation and PAM setup are documented in the ") + "<a href=\"" + Site.docs + "/dms-greeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">Dux Greeter docs.</a> " text: I18n.tr("Installation and PAM setup are documented in the %1.", "%1 is a link to the greeter docs").arg("<a href=\"" + Site.docs + "/dms-greeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">" + I18n.tr("Dux Greeter docs", "link text") + "</a>")
textFormat: Text.RichText textFormat: Text.RichText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -755,8 +755,8 @@ DDialog {
SettingsRow { SettingsRow {
iconName: "warning" iconName: "warning"
title: I18n.tr("This bind is overridden by config.kdl") title: I18n.tr("This bind is overridden by %1", "keybind conflict notice, %1 is the compositor config file").arg("config.kdl")
subtitle: I18n.tr("Config action: %1", "keybind conflict notice, %1 is the action from the compositor config").arg(root.configConflict?.action ?? "") + "\n" + I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl") subtitle: I18n.tr("Config action: %1", "keybind conflict notice, %1 is the action from the compositor config").arg(root.configConflict?.action ?? "") + "\n" + I18n.tr("To use this DMS bind, remove or change the keybind in your %1", "keybind conflict hint, %1 is the compositor config file").arg("config.kdl")
} }
} }
+2 -2
View File
@@ -344,7 +344,7 @@ Item {
const baselineDraft = editDraft; const baselineDraft = editDraft;
removeBindConfirm.showWithOptions({ removeBindConfirm.showWithOptions({
"title": I18n.tr("Remove Shortcut?"), "title": I18n.tr("Remove Shortcut?"),
"message": KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to dms/binds-user.lua so it stays removed across DMS updates.", "hyprland remove shortcut confirmation, %1 is the key combination").arg(key) : I18n.tr("Remove the shortcut %1?", "remove shortcut confirmation, %1 is the key combination").arg(key), "message": KeybindsService.currentProvider === "hyprland" ? I18n.tr("Remove the shortcut %1? An unbind entry will be saved to %2 so it stays removed across DMS updates.", "hyprland remove shortcut confirmation, %1 is the key combination, %2 the override file").arg(key).arg("dms/binds-user.lua") : I18n.tr("Remove the shortcut %1?", "remove shortcut confirmation, %1 is the key combination").arg(key),
"confirmText": I18n.tr("Remove"), "confirmText": I18n.tr("Remove"),
"confirmColor": Theme.primary, "confirmColor": Theme.primary,
"onConfirm": () => { "onConfirm": () => {
@@ -741,7 +741,7 @@ Item {
StyledText { StyledText {
text: { text: {
if (warningBox.showLegacy) if (warningBox.showLegacy)
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."); return I18n.tr("This install is still using %1. Run %2 to migrate before changing these settings.", "hyprland legacy config warning, %1 is the config file, %2 a command").arg("hyprland.conf").arg("dms setup");
if (warningBox.showSetup) if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.", "include setup banner, %1 is the dms config file name").arg("dms/binds"); return I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.", "include setup banner, %1 is the dms config file name").arg("dms/binds");
if (warningBox.showWarning) { if (warningBox.showWarning) {
@@ -106,7 +106,7 @@ Item {
case "missing_reader": case "missing_reader":
return I18n.tr("No fingerprint reader detected", "fingerprint setting status"); return I18n.tr("No fingerprint reader detected", "fingerprint setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available - install fprintd and pam_fprintd", "lock screen fingerprint setting"); return I18n.tr("Not available - install %1 and %2", "lock screen fingerprint setting, %1 and %2 are package names").arg("fprintd").arg("pam_fprintd");
default: default:
return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status"); return I18n.tr("Fingerprint availability could not be confirmed", "fingerprint setting status");
} }
@@ -119,7 +119,7 @@ Item {
case "missing_key_registration": case "missing_key_registration":
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status"); return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.", "security key setting status");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available - install or configure pam_u2f", "lock screen security key setting"); return I18n.tr("Not available - install or configure %1", "lock screen security key setting, %1 is a PAM module").arg("pam_u2f");
default: default:
return I18n.tr("Security-key availability could not be confirmed", "security key setting status"); return I18n.tr("Security-key availability could not be confirmed", "security key setting status");
} }
@@ -840,7 +840,7 @@ Item {
SettingsRow { SettingsRow {
visible: !SessionService.loginctlAvailable visible: !SessionService.loginctlAvailable
body: StyledText { body: StyledText {
text: I18n.tr("loginctl not available - lock integration requires DMS socket connection") text: I18n.tr("%1 not available - lock integration requires the DMS socket connection", "lock screen warning, %1 is the loginctl binary").arg("loginctl")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.warning color: Theme.warning
width: parent.width width: parent.width
@@ -75,7 +75,7 @@ Item {
settingKey: "bluetoothMpris" settingKey: "bluetoothMpris"
tags: ["bluetooth", "headphones", "media", "mpris", "avrcp"] tags: ["bluetooth", "headphones", "media", "mpris", "avrcp"]
text: I18n.tr("Bluetooth media controls", "Title for the setting that routes Bluetooth headset media buttons through DMS") text: I18n.tr("Bluetooth media controls", "Title for the setting that routes Bluetooth headset media buttons through DMS")
description: root.mprisProxyRunning ? I18n.tr("mpris-proxy is running and will create duplicate Bluetooth players. Disable it with: systemctl --user disable --now mpris-proxy.service", "Warning shown when the legacy BlueZ MPRIS proxy conflicts with DMS Bluetooth media controls") : I18n.tr("Route Bluetooth headset controls to the active DMS media player", "Description of how Bluetooth headset media buttons select a player") description: root.mprisProxyRunning ? I18n.tr("%1 is running and will create duplicate Bluetooth players. Disable it with: %2", "Warning shown when the legacy BlueZ MPRIS proxy conflicts with DMS Bluetooth media controls, %1 is the service name, %2 a shell command").arg("mpris-proxy").arg("systemctl --user disable --now mpris-proxy.service") : I18n.tr("Route Bluetooth headset controls to the active DMS media player", "Description of how Bluetooth headset media buttons select a player")
descriptionColor: root.mprisProxyRunning ? Theme.error : Theme.surfaceVariantText descriptionColor: root.mprisProxyRunning ? Theme.error : Theme.surfaceVariantText
checked: SettingsData.bluetoothMprisEnabled checked: SettingsData.bluetoothMprisEnabled
onToggled: checked => SettingsData.set("bluetoothMprisEnabled", checked) onToggled: checked => SettingsData.set("bluetoothMprisEnabled", checked)
@@ -86,7 +86,7 @@ Item {
} }
StyledText { StyledText {
text: I18n.tr("Click Import to add a .ovpn or .conf") text: I18n.tr("Click Import to add a %1 or %2 file", "VPN import hint, %1 and %2 are file extensions").arg(".ovpn").arg(".conf")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -28,7 +28,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
text: I18n.tr("Run an external command on each wallpaper change and merge its JSON palette into matugen under a namespace, so templates can use e.g. {{name.color0}}. The command receives the wallpaper as {image} / $DMS_WALLPAPER and the mode as {mode} / $DMS_MODE.", "injected palettes settings description, the placeholders in braces and the $DMS_ variables stay as is") text: I18n.tr("Run an external command on each wallpaper change and merge its JSON palette into %1 under a namespace, so templates can use e.g. %2. The command receives the wallpaper as %3 and the mode as %4.", "injected palettes settings description, %1 is matugen, %2 a template placeholder, %3 and %4 placeholder and variable pairs").arg("matugen").arg("{{name.color0}}").arg("{image} / $DMS_WALLPAPER").arg("{mode} / $DMS_MODE")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -1577,7 +1577,9 @@ RegistryBrowserWindow {
visible: (detailPane.plugin.permissions || []).length > 0 visible: (detailPane.plugin.permissions || []).length > 0
StyledText { StyledText {
text: I18n.tr("This plugin can", "plugin detail section, introduces the permission list") width: parent.width
text: I18n.tr("Declared by the author, not enforced", "plugin detail section, introduces the permission list the plugin declares about itself")
wrapMode: Text.WordWrap
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium font.weight: Theme.fontWeightMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -356,7 +356,7 @@ Column {
SettingsRow { SettingsRow {
iconName: "warning" iconName: "warning"
title: I18n.tr("Plugin manager unavailable") title: I18n.tr("Plugin manager unavailable")
subtitle: I18n.tr("The DMS_SOCKET environment variable is not set or the socket is unavailable. Automated plugin management requires the DMS_SOCKET.") subtitle: I18n.tr("The %1 environment variable is not set or the socket is unavailable. Automated plugin management requires it.", "plugin manager unavailable notice, %1 is an environment variable").arg("DMS_SOCKET")
} }
} }
@@ -61,7 +61,7 @@ FocusScope {
SettingsRow { SettingsRow {
body: StyledText { body: StyledText {
text: I18n.tr("Place plugin directories here. Each plugin should have a plugin.json manifest file.") text: I18n.tr("Place plugin directories here. Each plugin should have a %1 manifest file.", "plugin directory note, %1 is the manifest file name").arg("plugin.json")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
+3 -3
View File
@@ -49,7 +49,7 @@ Item {
SettingsNoteRow { SettingsNoteRow {
paintBackground: false paintBackground: false
noteIconName: "info" noteIconName: "info"
text: I18n.tr("The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf|qtengine/config.json and ~/.config/gtk-3.0|gtk-4.0).") text: I18n.tr("The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (%1 and %2).", "theme apps backup note, %1 and %2 are config paths").arg("qt5ct.conf|qt6ct.conf|qtengine/config.json").arg("~/.config/gtk-3.0|gtk-4.0")
} }
SettingsCard { SettingsCard {
@@ -184,7 +184,7 @@ Item {
tags: ["matugen", "user", "templates"] tags: ["matugen", "user", "templates"]
settingKey: "runUserMatugenTemplates" settingKey: "runUserMatugenTemplates"
text: I18n.tr("Run user templates") text: I18n.tr("Run user templates")
description: I18n.tr("Also runs the templates in your matugen config.toml", "run user matugen templates toggle description") description: I18n.tr("Also runs the templates in your %1", "run user matugen templates toggle description, %1 is the matugen config file").arg("matugen config.toml")
checked: SettingsData.runUserMatugenTemplates checked: SettingsData.runUserMatugenTemplates
onToggled: checked => SettingsData.set("runUserMatugenTemplates", checked) onToggled: checked => SettingsData.set("runUserMatugenTemplates", checked)
} }
@@ -413,7 +413,7 @@ Item {
tags: ["matugen", "neovim", "terminal", "template"] tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovim" settingKey: "matugenTemplateNeovim"
text: "neovim" text: "neovim"
description: root.getTemplateDescription("nvim", I18n.tr("Required plugin: ") + "https://github.com/duxlabs-dev/base46") description: root.getTemplateDescription("nvim", I18n.tr("Required plugin: %1", "%1 is a repository URL").arg("https://github.com/duxlabs-dev/base46"))
descriptionColor: root.getTemplateDescriptionColor("nvim") descriptionColor: root.getTemplateDescriptionColor("nvim")
checked: SettingsData.matugenTemplateNeovim checked: SettingsData.matugenTemplateNeovim
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked) onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
@@ -163,7 +163,10 @@ Item {
break; break;
case 1: case 1:
if (ToastService.wallpaperErrorStatus === "matugen_missing" || ToastService.wallpaperErrorStatus === "error") { if (ToastService.wallpaperErrorStatus === "matugen_missing" || ToastService.wallpaperErrorStatus === "error") {
ToastService.showError(ToastService.wallpaperErrorStatus === "matugen_missing" ? I18n.tr("matugen not found - install matugen package for dynamic theming", "matugen error") : I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error")); if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError(I18n.tr("%1 not found", "missing program, %1 is a program name").arg("matugen"), I18n.tr("Install the %1 package for dynamic theming", "matugen installation hint, %1 is the package name").arg("matugen"));
else
ToastService.showError(I18n.tr("Wallpaper processing failed - check wallpaper path", "wallpaper error"));
break; break;
} }
Theme.switchThemeCategory("dynamic", Theme.dynamic); Theme.switchThemeCategory("dynamic", Theme.dynamic);
@@ -284,7 +287,7 @@ Item {
if (ToastService.wallpaperErrorStatus === "error") if (ToastService.wallpaperErrorStatus === "error")
return I18n.tr("Wallpaper processing failed", "wallpaper processing error"); return I18n.tr("Wallpaper processing failed", "wallpaper processing error");
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
return I18n.tr("Install matugen package for dynamic theming", "matugen installation hint"); return I18n.tr("Install the %1 package for dynamic theming", "matugen installation hint, %1 is the package name").arg("matugen");
if (Theme.wallpaperPath) if (Theme.wallpaperPath)
return Theme.wallpaperPath; return Theme.wallpaperPath;
return I18n.tr("Dynamic colors from wallpaper", "dynamic colors description"); return I18n.tr("Dynamic colors from wallpaper", "dynamic colors description");
+1 -1
View File
@@ -105,7 +105,7 @@ Item {
const enableGreeter = !userRow.modelData.isGreeter; const enableGreeter = !userRow.modelData.isGreeter;
greeterToggleConfirm.showWithOptions({ greeterToggleConfirm.showWithOptions({
title: enableGreeter ? I18n.tr("Allow greeter access?") : I18n.tr("Remove greeter access?"), title: enableGreeter ? I18n.tr("Allow greeter access?") : I18n.tr("Remove greeter access?"),
message: enableGreeter ? I18n.tr("Add \"%1\" to the %2 group? They must log out and back in, then run dms-greeter sync --profile to publish their login-screen theme.", "confirm dialog message, %1 is the username, %2 is the group name, keep the command verbatim").arg(userRow.modelData.username).arg(UsersService.greeterGroup) : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.greeterGroup), message: enableGreeter ? I18n.tr("Add \"%1\" to the %2 group? They must log out and back in, then run %3 to publish their login-screen theme.", "confirm dialog message, %1 is the username, %2 is the group name, %3 a command").arg(userRow.modelData.username).arg(UsersService.greeterGroup).arg("dms-greeter sync --profile") : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.greeterGroup),
confirmText: enableGreeter ? I18n.tr("Allow", "verb, confirm button granting a user greeter access") : I18n.tr("Remove"), confirmText: enableGreeter ? I18n.tr("Allow", "verb, confirm button granting a user greeter access") : I18n.tr("Remove"),
confirmColor: Theme.primary, confirmColor: Theme.primary,
onConfirm: () => { onConfirm: () => {
@@ -229,7 +229,7 @@ Column {
function warnIfMissingQtTheme() { function warnIfMissingQtTheme() {
if (Quickshell.env("QT_QPA_PLATFORMTHEME") === "gtk3" || Quickshell.env("QT_QPA_PLATFORMTHEME") === "qt6ct" || Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") === "qt6ct" || SettingsData.qtengineActive || Quickshell.env("QT_QPA_PLATFORMTHEME") === "kde") if (Quickshell.env("QT_QPA_PLATFORMTHEME") === "gtk3" || Quickshell.env("QT_QPA_PLATFORMTHEME") === "qt6ct" || Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") === "qt6ct" || SettingsData.qtengineActive || Quickshell.env("QT_QPA_PLATFORMTHEME") === "kde")
return; return;
ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set one of:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct OR\nQT_QPA_PLATFORMTHEME=qtengine\nas environment variables, and then restart the shell.\n\nOnly qt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("Set one of these environment variables, then restart the shell:\n%1\n\nOnly %2 requires %3 to be installed.", "qt theme env error body, %1 is a list of variable assignments, %2 and %3 are package names").arg(["QT_QPA_PLATFORMTHEME=gtk3", "QT_QPA_PLATFORMTHEME=qt6ct", "QT_QPA_PLATFORMTHEME=qtengine"].join("\n")).arg("qt6ct").arg("qt6ct-kde"));
} }
SettingsCard { SettingsCard {
@@ -399,12 +399,16 @@ Column {
readonly property var fillModes: ["Stretch", "Fit", "Fill", "Scrolling", "Tile", "TileVertically", "TileHorizontally", "Pad"] readonly property var fillModes: ["Stretch", "Fit", "Fill", "Scrolling", "Tile", "TileVertically", "TileHorizontally", "Pad"]
readonly property var fillModeLabels: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Scroll", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile Vertically", "wallpaper fill mode"), I18n.tr("Tile Horizontally", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")] readonly property var fillModeLabels: [I18n.tr("Stretch", "wallpaper fill mode"), I18n.tr("Fit", "wallpaper fill mode"), I18n.tr("Fill", "wallpaper fill mode"), I18n.tr("Scroll", "wallpaper fill mode"), I18n.tr("Tile", "wallpaper fill mode"), I18n.tr("Tile Vertically", "wallpaper fill mode"), I18n.tr("Tile Horizontally", "wallpaper fill mode"), I18n.tr("Pad", "wallpaper fill mode")]
// dms picks the animation driver at launch from this setting
readonly property bool simpleAnimationDriver: Quickshell.env("QSG_USE_SIMPLE_ANIMATION_DRIVER") === "1"
readonly property bool driverMismatch: (SettingsData.wallpaperFillMode === "Scrolling") !== simpleAnimationDriver
tab: "wallpaper" tab: "wallpaper"
tags: ["background", "fill", "fit", "stretch", "tile", "scale"] tags: ["background", "fill", "fit", "stretch", "tile", "scale"]
settingKey: "wallpaperFillMode" settingKey: "wallpaperFillMode"
resetKeys: root.perMonitor ? [] : ["wallpaperFillMode"] resetKeys: root.perMonitor ? [] : ["wallpaperFillMode"]
text: I18n.tr("Fill mode") text: I18n.tr("Fill mode")
description: driverMismatch ? I18n.tr("Requires restart") : ""
visible: root.wallpaperIsImage visible: root.wallpaperIsImage
dropdownWidth: 190 dropdownWidth: 190
options: fillModeLabels options: fillModeLabels
@@ -644,7 +648,7 @@ Column {
SettingsNoteRow { SettingsNoteRow {
visible: !Theme.matugenAvailable visible: !Theme.matugenAvailable
text: I18n.tr("Install matugen package for dynamic theming", "matugen installation hint") text: I18n.tr("Install the %1 package for dynamic theming", "matugen installation hint, %1 is the package name").arg("matugen")
} }
SettingsRow { SettingsRow {
@@ -1082,7 +1086,7 @@ Column {
settingKey: "blurredWallpaperLayer" settingKey: "blurredWallpaperLayer"
visible: CompositorService.isNiri visible: CompositorService.isNiri
text: I18n.tr("Blur layer") text: I18n.tr("Blur layer")
description: I18n.tr("Layer namespace dms:blurwallpaper, needs a niri blur rule") description: I18n.tr("Layer namespace %1, needs a niri blur rule", "blur layer toggle description, %1 is the layer namespace").arg("dms:blurwallpaper")
checked: SettingsData.blurredWallpaperLayer checked: SettingsData.blurredWallpaperLayer
onToggled: checked => SettingsData.set("blurredWallpaperLayer", checked) onToggled: checked => SettingsData.set("blurredWallpaperLayer", checked)
} }
@@ -68,7 +68,7 @@ QtObject {
} }
function showReadOnlyWarning() { function showReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", warningCategory); ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using %1. Run %2 to migrate before changing these settings.", "hyprland legacy config warning, %1 is the config file, %2 a command").arg("hyprland.conf").arg("dms setup"), "dms setup", warningCategory);
} }
function fix() { function fix() {
@@ -47,7 +47,7 @@ StyledRect {
} }
StyledText { StyledText {
text: root.showLegacy ? I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings.") : I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(root.include.fragmentLabel) text: root.showLegacy ? I18n.tr("This install is still using %1. Run %2 to migrate before changing these settings.", "hyprland legacy config warning, %1 is the config file, %2 a command").arg("hyprland.conf").arg("dms setup") : I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg(root.include.fragmentLabel)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -17,10 +17,7 @@ SettingsDropdownRow {
function _enumerate() { function _enumerate() {
if (_enumerated) if (_enumerated)
return; return;
const bundled = DCommon.Fonts.bundledFamilies.filter(f => f !== root.defaultFamily); _families = ["Default"].concat(DCommon.Fonts.selectableFamilies(root.defaultFamily));
const system = Qt.fontFamilies().filter(f => !f.startsWith(".") && f !== root.defaultFamily && !bundled.includes(f));
system.sort();
_families = ["Default"].concat(bundled, system);
_enumerated = true; _enumerated = true;
} }
+1 -1
View File
@@ -69,7 +69,7 @@ Item {
} }
readonly property var widgetAvailability: { readonly property var widgetAvailability: {
const dgopWarning = DgopService.dgopAvailable ? undefined : I18n.tr("Requires 'dgop' tool"); const dgopWarning = DgopService.dgopAvailable ? undefined : I18n.tr("Requires %1", "missing program").arg("dgop");
return { return {
"layout": { "layout": {
"enabled": CompositorService.isMango && MangoService.available, "enabled": CompositorService.isMango && MangoService.available,
@@ -335,7 +335,7 @@ Item {
} }
function showHyprlandReadOnlyWarning() { function showHyprlandReadOnlyWarning() {
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "hyprland-migration"); ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using %1. Run %2 to migrate before changing these settings.", "hyprland legacy config warning, %1 is the config file, %2 a command").arg("hyprland.conf").arg("dms setup"), "dms setup", "hyprland-migration");
} }
Component.onCompleted: { Component.onCompleted: {

Some files were not shown because too many files have changed in this diff Show More