1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-24 12:05:21 -04:00

Compare commits

..

6 Commits

Author SHA1 Message Date
purian23 aed731efb0 fix(clipboard): restore Save button targets in editor 2026-05-25 23:19:42 -04:00
purian23 cf0632c077 feat(Clipboard): Revive ClipboardEditor PR
- Original PR #1916 by @nabaco
2026-05-24 23:28:21 -04:00
Nachum Barcohen e92da4a15f Show full clipboard text in editor 2026-05-24 22:34:24 -04:00
Nachum Barcohen 8abdff3220 Add clipboard editor shortcuts and hints 2026-05-24 22:34:24 -04:00
Nachum Barcohen 584d57a8de Add split save menu for clipboard editor 2026-05-24 22:34:05 -04:00
Nachum Barcohen afb5e59c29 feat(clipboard): Add editing capability to clipboard entries 2026-05-24 22:34:05 -04:00
185 changed files with 7679 additions and 29637 deletions
+1 -1
View File
@@ -26,4 +26,4 @@ jobs:
go-version-file: core/go.mod
- name: run pre-commit hooks
uses: j178/prek-action@v2
uses: j178/prek-action@v1
+2 -10
View File
@@ -54,10 +54,8 @@ func init() {
}
type IncludeResult struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Exists bool `json:"exists"`
Included bool `json:"included"`
}
func runResolveInclude(cmd *cobra.Command, args []string) {
@@ -108,8 +106,6 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
mainLua := filepath.Join(configDir, "hyprland.lua")
if _, err := os.Stat(mainLua); err == nil {
result.ConfigFormat = "lua"
result.ReadOnly = false
processedLua := make(map[string]bool)
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
result.Included = true
@@ -119,10 +115,6 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
mainConf := filepath.Join(configDir, "hyprland.conf")
if _, err := os.Stat(mainConf); err == nil {
if result.ConfigFormat == "" {
result.ConfigFormat = "hyprlang"
result.ReadOnly = true
}
processed := make(map[string]bool)
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
result.Included = true
+1 -4
View File
@@ -947,12 +947,9 @@ func checkSystemdServices() []checkResult {
message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active)
}
switch {
case dmsState.active == "failed":
status = statusError
case dmsState.active == "active":
case dmsState.enabled == "disabled":
status, message = statusWarn, "Disabled"
case dmsState.active == "inactive":
case dmsState.active == "failed" || dmsState.active == "inactive":
status = statusError
}
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
+11 -113
View File
@@ -3,7 +3,6 @@ package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
@@ -60,36 +59,22 @@ var greeterInstallCmd = &cobra.Command{
}
var greeterSyncCmd = &cobra.Command{
Use: "sync",
Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen. Also updates a per-user cache slot at users/<username>/ for multi-account greeter theme preview.\n\nUse --profile on secondary accounts to sync only your own users/<username>/ slot without sudo or greetd changes.",
PreRunE: func(cmd *cobra.Command, args []string) error {
profile, _ := cmd.Flags().GetBool("profile")
if profile {
return nil
}
return preRunPrivileged(cmd, args)
},
Use: "sync",
Short: "Sync DMS theme and settings with greeter",
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
PreRunE: preRunPrivileged,
Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes")
auth, _ := cmd.Flags().GetBool("auth")
local, _ := cmd.Flags().GetBool("local")
profile, _ := cmd.Flags().GetBool("profile")
autologinOnly, _ := cmd.Flags().GetBool("autologin-only")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncInTerminal(yes, auth, local, profile, autologinOnly); err != nil {
if err := syncInTerminal(yes, auth, local); err != nil {
log.Fatalf("Error launching sync in terminal: %v", err)
}
return
}
if autologinOnly {
if err := syncGreeterAutoLoginOnly(yes); err != nil {
log.Fatalf("Error syncing greeter auto-login: %v", err)
}
return
}
if err := syncGreeter(yes, auth, local, profile); err != nil {
if err := syncGreeter(yes, auth, local); err != nil {
log.Fatalf("Error syncing greeter: %v", err)
}
},
@@ -100,8 +85,6 @@ func init() {
greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done")
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
greeterSyncCmd.Flags().BoolP("profile", "p", false, "Sync only your per-user greeter slot (no sudo; for secondary accounts)")
greeterSyncCmd.Flags().Bool("autologin-only", false, "Apply only greeter auto-login on startup settings to greetd (no theme or auth sync)")
}
var greeterEnableCmd = &cobra.Command{
@@ -529,8 +512,8 @@ func runCommandInTerminal(shellCmd string) error {
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
}
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly bool, autologinOnly bool) error {
syncFlags := make([]string, 0, 5)
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
syncFlags := make([]string, 0, 3)
if nonInteractive {
syncFlags = append(syncFlags, "--yes")
}
@@ -540,22 +523,11 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly
if local {
syncFlags = append(syncFlags, "--local")
}
if profileOnly {
syncFlags = append(syncFlags, "--profile")
}
if autologinOnly {
syncFlags = append(syncFlags, "--autologin-only")
}
shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ")
}
var shellCmd string
if autologinOnly {
shellCmd = shellSyncCmd + `; echo; echo "Auto-login update finished. Closing in 3 seconds..."; sleep 3`
} else {
shellCmd = shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
}
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd)
}
@@ -569,54 +541,7 @@ func resolveLocalWrapperShell() (string, error) {
return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper")
}
func syncGreeterAutoLoginOnly(nonInteractive bool) error {
logFunc := func(msg string) {
fmt.Println(msg)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
cacheSettingsPath := filepath.Join(greeter.GreeterCacheDir, "settings.json")
enabled := false
for _, path := range []string{cacheSettingsPath, settingsPath} {
data, readErr := os.ReadFile(path)
if readErr != nil {
continue
}
var cfg struct {
GreeterAutoLogin bool `json:"greeterAutoLogin"`
}
if json.Unmarshal(data, &cfg) == nil {
enabled = cfg.GreeterAutoLogin
break
}
}
fmt.Println("=== Greeter Auto-Login ===")
fmt.Println()
if enabled {
fmt.Println("Enabling auto-login on startup in greetd.")
fmt.Println("After your next reboot, DMS will skip the greeter password until you sign out.")
} else {
fmt.Println("Disabling auto-login on startup in greetd.")
fmt.Println("After your next reboot, you will enter your password at the greeter again.")
}
fmt.Println()
fmt.Println("Administrator (sudo) access is required to update /etc/greetd/config.toml.")
fmt.Println()
return greeter.SyncGreeterAutoLoginOnly(logFunc, "")
}
func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bool) error {
if profileOnly {
return syncGreeterProfileOnly(nonInteractive)
}
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if !nonInteractive {
fmt.Println("=== DMS Greeter Sync ===")
fmt.Println()
@@ -827,26 +752,6 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool, profileOnly bo
return nil
}
func syncGreeterProfileOnly(nonInteractive bool) error {
logFunc := func(msg string) {
fmt.Println(msg)
}
if !nonInteractive {
fmt.Println("=== DMS Greeter Profile Sync ===")
fmt.Println()
fmt.Println("Syncing your personal greeter theme slot (no system changes)...")
}
if err := greeter.SyncUserProfileCache(logFunc); err != nil {
return err
}
if !nonInteractive {
fmt.Println("\n=== Profile Sync Complete ===")
fmt.Println("\nYour theme, wallpaper, and profile photo have been synced for the login screen.")
fmt.Println("Log out to preview your greeter look when selecting your account.")
}
return nil
}
func hasDmsShellQml(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "shell.qml"))
return err == nil && !info.IsDir()
@@ -932,14 +837,7 @@ func resolveLocalDMSPath() (string, error) {
}
}
configuredCommand := readDefaultSessionCommand("/etc/greetd/config.toml")
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
if resolved, ok := resolveDMSLocalCandidate(pathOverride); ok {
return resolved, nil
}
}
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root, set DMS_LOCAL_PATH=/absolute/path/to/repo, or configure greetd with -p /path/to/quickshell", wd)
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd)
}
func disableDisplayManager(dmName string) (bool, error) {
+2 -34
View File
@@ -4,9 +4,7 @@ import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
@@ -181,39 +179,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
return config
}
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. Best-effort.
func setPopoutScreenshotMode(begin bool) {
fn := "end"
if begin {
fn = "begin"
}
cmdArgs := []string{"ipc"}
if pid, ok := getFirstDMSPID(); ok {
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
} else {
if err := findConfig(nil, nil); err != nil {
return
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
}
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
_ = exec.Command("qs", cmdArgs...).Run()
}
func runScreenshot(config screenshot.Config) {
// Region select needs the keyboard; drop popout grabs for its duration.
result, err := func() (*screenshot.CaptureResult, error) {
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion
if interactive {
setPopoutScreenshotMode(true)
defer setPopoutScreenshotMode(false)
}
return screenshot.New(config).Run()
}()
sc := screenshot.New(config)
result, err := sc.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
-6
View File
@@ -12,10 +12,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
// overhead in the line-delimited IPC response.
const maxIPCMessageSize = 96 * 1024 * 1024
func sendServerRequest(req models.Request) (*models.Response[any], error) {
socketPath := getServerSocketPath()
@@ -26,7 +22,6 @@ func sendServerRequest(req models.Request) (*models.Response[any], error) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
@@ -66,7 +61,6 @@ func sendServerRequestFireAndForget(req models.Request) error {
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), maxIPCMessageSize)
scanner.Scan() // discard initial capabilities message
reqData, err := json.Marshal(req)
-4
View File
@@ -600,10 +600,6 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
return result, result.Error
}
CleanupStrayHyprlandConfFile(func(format string, v ...any) {
cd.log(fmt.Sprintf(format, v...))
})
result.Deployed = true
cd.log("Successfully deployed Hyprland configuration")
return result, nil
+3 -15
View File
@@ -20,17 +20,13 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
td := t.TempDir()
t.Setenv("HOME", td)
configDir := filepath.Join(td, ".config", "hypr")
dmsDir := filepath.Join(configDir, "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
require.NoError(t, os.MkdirAll(configDir, 0o755))
confPath := filepath.Join(configDir, "hyprland.conf")
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644))
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
CleanupStrayHyprlandConfFile(nil)
assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated")
assert.FileExists(t, dmsConfPath, "must not touch dms/*.conf when user has not migrated")
assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName))
})
@@ -38,25 +34,20 @@ func TestCleanupStrayHyprlandConfFile(t *testing.T) {
td := t.TempDir()
t.Setenv("HOME", td)
configDir := filepath.Join(td, ".config", "hypr")
dmsDir := filepath.Join(configDir, "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
require.NoError(t, os.MkdirAll(configDir, 0o755))
luaPath := filepath.Join(configDir, "hyprland.lua")
require.NoError(t, os.WriteFile(luaPath, []byte("-- dms managed\n"), 0o644))
confPath := filepath.Join(configDir, "hyprland.conf")
dmsConfPath := filepath.Join(dmsDir, "colors.conf")
require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644))
require.NoError(t, os.WriteFile(dmsConfPath, []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
CleanupStrayHyprlandConfFile(nil)
assert.NoFileExists(t, confPath)
assert.NoFileExists(t, dmsConfPath)
assert.FileExists(t, luaPath)
entries, err := os.ReadDir(filepath.Join(configDir, hyprlandBackupDirName))
require.NoError(t, err)
require.Len(t, entries, 1)
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "hyprland.conf"))
assert.FileExists(t, filepath.Join(configDir, hyprlandBackupDirName, entries[0].Name(), "dms", "colors.conf"))
})
}
@@ -413,7 +404,6 @@ general {
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf"), []byte("bind = SUPER, T, exec, foot\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "colors.conf"), []byte("$primary = rgba(d0bcffFF)\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "cursor.conf"), []byte("env = XCURSOR_SIZE,24\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"), []byte("old backup\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.conf.backup.old"), []byte("old dms backup\n"), 0o644))
@@ -433,12 +423,10 @@ general {
assert.Contains(t, result.BackupPath, hyprlandBackupDirName)
assert.NoFileExists(t, hyprPath)
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "colors.conf"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "cursor.conf"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf.backup.old"))
assert.FileExists(t, filepath.Join(filepath.Dir(result.BackupPath), "dms", "binds.conf.backup.old"))
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf"))
assert.NoFileExists(t, filepath.Join(dmsDir, "colors.conf"))
assert.NoFileExists(t, filepath.Join(dmsDir, "cursor.conf"))
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
@@ -497,7 +485,7 @@ general {
managed, err := os.ReadFile(filepath.Join(dmsDir, "binds.lua"))
require.NoError(t, err)
assert.Contains(t, string(managed), `hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))`)
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })`)
assert.Contains(t, string(managed), `hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })`)
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
require.NoError(t, err)
+6 -9
View File
@@ -38,7 +38,7 @@ hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd([[dms ipc call brightness increme
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd([[dms ipc call brightness decrement 5 ""]]), { locked = true, repeating = true })
-- === Window Management ===
hl.bind("SUPER + Q", hl.dsp.window.close())
hl.bind("SUPER + Q", hl.dsp.window.kill())
hl.bind("SUPER + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" }))
hl.bind("SUPER + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" }))
hl.bind("SUPER + SHIFT + T", hl.dsp.window.float({ action = "toggle" }))
@@ -112,9 +112,6 @@ hl.bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
hl.bind("SUPER + CTRL + mouse_down", hl.dsp.window.move({ workspace = "e+1" }))
hl.bind("SUPER + CTRL + mouse_up", hl.dsp.window.move({ workspace = "e-1" }))
-- === Touchpad Gestures ===
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
-- === Numbered Workspaces ===
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))
hl.bind("SUPER + 2", hl.dsp.focus({ workspace = "2" }))
@@ -143,7 +140,7 @@ hl.bind("SUPER + bracketright", hl.dsp.layout("preselect r"))
-- === Sizing & Layout ===
hl.bind("SUPER + R", hl.dsp.layout("togglesplit"))
hl.bind("SUPER + CTRL + F", hl.dsp.window.fullscreen({ mode = "maximized", action = "set" }))
hl.bind("SUPER + CTRL + F", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive exact 100% 100%]]))
-- === Move/resize windows with mainMod + LMB/RMB and dragging ===
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window" })
@@ -153,10 +150,10 @@ hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = tr
hl.bind("SUPER + code:21", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { description = "Shrink window left" })
-- === Manual Sizing ===
hl.bind("SUPER + minus", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { repeating = true })
hl.bind("SUPER + equal", hl.dsp.window.resize({ x = 100, y = 0, relative = true }), { repeating = true })
hl.bind("SUPER + SHIFT + minus", hl.dsp.window.resize({ x = 0, y = -100, relative = true }), { repeating = true })
hl.bind("SUPER + SHIFT + equal", hl.dsp.window.resize({ x = 0, y = 100, relative = true }), { repeating = true })
hl.bind("SUPER + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })
hl.bind("SUPER + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 10% 0]]), { repeating = true })
hl.bind("SUPER + SHIFT + minus", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 -10%]]), { repeating = true })
hl.bind("SUPER + SHIFT + equal", hl.dsp.exec_cmd([[hyprctl dispatch resizeactive 0 10%]]), { repeating = true })
-- === Screenshots ===
hl.bind("Print", hl.dsp.exec_cmd("dms screenshot"))
@@ -13,10 +13,6 @@ hl.config({
input = {
kb_layout = "us",
numlock_by_default = true,
touchpad = {
tap_to_click = true,
natural_scroll = true,
},
},
general = {
gaps_in = 5,
+12 -35
View File
@@ -138,9 +138,11 @@ func readExistingHyprlandConfig(configDir string) (data string, sourcePath strin
return "", "", nil
}
// CleanupStrayHyprlandConfFile moves stray ~/.config/hypr/hyprland.conf and
// top-level ~/.config/hypr/dms/*.conf files into .dms-backups/<timestamp>/ only
// when hyprland.lua also exists as the live config.
// CleanupStrayHyprlandConfFile moves a stray ~/.config/hypr/hyprland.conf
// into .dms-backups/<timestamp>/ only when hyprland.lua also exists, which
// proves Lua is the live config and the .conf is an autogen Hyprland 0.55
// produced when launched without -c. If only hyprland.conf exists, the user
// has not migrated and we must leave their config alone.
func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
return
@@ -154,44 +156,19 @@ func CleanupStrayHyprlandConfFile(logFn func(format string, v ...any)) {
if _, err := os.Stat(luaPath); err != nil {
return
}
var strayPaths []string
confPath := filepath.Join(configDir, "hyprland.conf")
if info, err := os.Lstat(confPath); err == nil && !info.IsDir() {
strayPaths = append(strayPaths, confPath)
}
dmsConfPaths, err := filepath.Glob(filepath.Join(configDir, "dms", "*.conf"))
if err == nil {
for _, p := range dmsConfPaths {
if info, err := os.Lstat(p); err == nil && !info.IsDir() {
strayPaths = append(strayPaths, p)
}
}
}
if len(strayPaths) == 0 {
if _, err := os.Stat(confPath); err != nil {
return
}
ts := time.Now().Format("2006-01-02_15-04-05")
moved := 0
for _, src := range strayPaths {
rel, err := filepath.Rel(configDir, src)
if err != nil {
rel = filepath.Base(src)
}
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, rel)
if err := moveHyprlandConfigFile(src, dst); err != nil {
if logFn != nil {
logFn("Could not move stray Hyprland conf file %s: %v", src, err)
}
continue
}
moved++
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf")
if err := moveHyprlandConfigFile(confPath, dst); err != nil {
if logFn != nil {
logFn("Moved stray Hyprland conf file to %s", dst)
logFn("Could not move stray hyprland.conf: %v", err)
}
return
}
if moved > 0 && logFn != nil {
logFn("Moved %d stray Hyprland conf file(s) out of the active Lua config tree", moved)
if logFn != nil {
logFn("Moved stray hyprland.conf to %s", dst)
}
}
+19 -443
View File
@@ -9,7 +9,6 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"time"
@@ -192,421 +191,6 @@ func upsertDefaultSession(configContent, greeterUser, command string) string {
return strings.Join(out, "\n")
}
func removeTomlSection(configContent, sectionName string) string {
lines := strings.Split(configContent, "\n")
var out []string
inSection := false
for _, line := range lines {
if section, ok := parseTomlSection(line); ok {
inSection = section == sectionName
if inSection {
continue
}
out = append(out, line)
continue
}
if inSection {
continue
}
out = append(out, line)
}
result := strings.TrimRight(strings.Join(out, "\n"), "\n")
if result != "" {
result += "\n"
}
return result
}
func stripDesktopExecCodes(execLine string) string {
fields := strings.Fields(execLine)
cleaned := make([]string, 0, len(fields))
for _, field := range fields {
if strings.HasPrefix(field, "%") {
continue
}
cleaned = append(cleaned, field)
}
return strings.Join(cleaned, " ")
}
func formatInitialSessionCommand(sessionExec string) string {
execLine := strings.TrimSpace(stripDesktopExecCodes(sessionExec))
if execLine == "" {
return `command = ""`
}
escaped := strings.ReplaceAll(execLine, `'`, `'\''`)
inner := fmt.Sprintf("env XDG_SESSION_TYPE=wayland sh -c 'exec %s'", escaped)
tomlEscaped := strings.ReplaceAll(inner, `\`, `\\`)
tomlEscaped = strings.ReplaceAll(tomlEscaped, `"`, `\"`)
return fmt.Sprintf(`command = "%s"`, tomlEscaped)
}
func upsertInitialSession(configContent, loginUser, sessionExec string, enabled bool) string {
if !enabled {
return removeTomlSection(configContent, "initial_session")
}
commandLine := formatInitialSessionCommand(sessionExec)
lines := strings.Split(configContent, "\n")
var out []string
inInitialSession := false
foundInitialSession := false
initialSessionUserSet := false
initialSessionCommandSet := false
appendInitialSessionFields := func() {
if !initialSessionUserSet {
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
}
if !initialSessionCommandSet {
out = append(out, commandLine)
}
}
for _, line := range lines {
if section, ok := parseTomlSection(line); ok {
if inInitialSession {
appendInitialSessionFields()
}
inInitialSession = section == "initial_session"
if inInitialSession {
foundInitialSession = true
initialSessionUserSet = false
initialSessionCommandSet = false
}
out = append(out, line)
continue
}
if inInitialSession {
trimmed := stripTomlComment(line)
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
initialSessionUserSet = true
continue
}
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
if !initialSessionCommandSet {
out = append(out, commandLine)
initialSessionCommandSet = true
}
continue
}
}
out = append(out, line)
}
if inInitialSession {
appendInitialSessionFields()
}
if !foundInitialSession {
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
out = append(out, "")
}
out = append(out, "[initial_session]")
out = append(out, fmt.Sprintf(`user = "%s"`, loginUser))
out = append(out, commandLine)
}
return strings.Join(out, "\n")
}
type greeterAutoLoginConfig struct {
GreeterAutoLogin bool `json:"greeterAutoLogin"`
GreeterRememberLastUser bool `json:"greeterRememberLastUser"`
GreeterRememberLastSession bool `json:"greeterRememberLastSession"`
}
type greeterAutoLoginMemory struct {
LastSuccessfulUser string `json:"lastSuccessfulUser"`
LastSessionID string `json:"lastSessionId"`
LastSessionExec string `json:"lastSessionExec"`
AutoLoginEnabled bool `json:"autoLoginEnabled"`
}
func readGreeterAutoLoginConfig(settingsPath string) (greeterAutoLoginConfig, error) {
cfg := greeterAutoLoginConfig{
GreeterRememberLastUser: true,
GreeterRememberLastSession: true,
}
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
return cfg, nil
}
func readGreeterAutoLoginMemory(memoryPath string) (greeterAutoLoginMemory, error) {
var mem greeterAutoLoginMemory
data, err := os.ReadFile(memoryPath)
if err != nil {
if os.IsNotExist(err) {
return mem, nil
}
return mem, err
}
if err := json.Unmarshal(data, &mem); err != nil {
return mem, fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err)
}
return mem, nil
}
func execFromDesktopFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "Exec=") {
return strings.TrimSpace(trimmed[len("Exec="):]), nil
}
}
return "", fmt.Errorf("no Exec= line found in %s", path)
}
func resolveGreeterAutoLoginState(cacheDir, homeDir string) (enabled bool, loginUser string, sessionExec string, err error) {
settingsPath := filepath.Join(cacheDir, "settings.json")
if _, statErr := os.Stat(settingsPath); statErr != nil {
settingsPath = filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
}
cfg, err := readGreeterAutoLoginConfig(settingsPath)
if err != nil {
return false, "", "", err
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
mem, err := readGreeterAutoLoginMemory(memoryPath)
if err != nil {
return false, "", "", err
}
enabled = cfg.GreeterAutoLogin
if !enabled {
return false, "", "", nil
}
if !cfg.GreeterRememberLastUser || !cfg.GreeterRememberLastSession {
return true, "", "", nil
}
loginUser = mem.LastSuccessfulUser
if loginUser == "" {
current, userErr := user.Current()
if userErr != nil {
return true, "", "", userErr
}
loginUser = current.Username
}
sessionExec = mem.LastSessionExec
if sessionExec == "" && mem.LastSessionID != "" {
sessionExec, err = execFromDesktopFile(mem.LastSessionID)
if err != nil {
sessionExec = ""
}
}
return true, loginUser, sessionExec, nil
}
func writeGreetdConfig(configPath, content string, logFunc func(string), sudoPassword, successMsg string) error {
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup greetd config: %w", err)
}
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(content); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greetd config: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greetd config: %w", err)
}
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err)
}
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to install greetd config: %w", err)
}
if logFunc != nil && successMsg != "" {
logFunc(successMsg)
}
return nil
}
func clearGreeterAutoLoginMemory(memoryPath, sudoPassword string) error {
data, err := readGreeterMemoryFile(memoryPath, sudoPassword)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if len(strings.TrimSpace(string(data))) == 0 {
return nil
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("failed to parse greeter memory at %s: %w", memoryPath, err)
}
if _, ok := raw["autoLoginEnabled"]; !ok {
return nil
}
delete(raw, "autoLoginEnabled")
encoded, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return err
}
if len(encoded) == 0 || string(encoded) == "null" {
encoded = []byte("{}")
}
encoded = append(encoded, '\n')
if err := os.WriteFile(memoryPath, encoded, 0o644); err == nil {
return nil
} else if !os.IsPermission(err) {
return err
}
tmpFile, err := os.CreateTemp("", "greeter-memory-*.json")
if err != nil {
return fmt.Errorf("failed to create temp greeter memory file: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.Write(encoded); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greeter memory file: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greeter memory file: %w", err)
}
greeterUser := DetectGreeterUser()
greeterGroup := DetectGreeterGroup()
owner := greeterUser + ":" + greeterGroup
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", greeterUser, "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0664", tmpFile.Name(), memoryPath); fallbackErr != nil {
return fmt.Errorf("failed to install greeter memory file (preferred %s: %w; fallback root:%s: %v)", owner, err, greeterGroup, fallbackErr)
}
}
return nil
}
func readGreeterMemoryFile(memoryPath, sudoPassword string) ([]byte, error) {
data, err := os.ReadFile(memoryPath)
if err == nil || !os.IsPermission(err) {
return data, err
}
tmpFile, err := os.CreateTemp("", "greeter-memory-read-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp file for greeter memory read: %w", err)
}
tmpPath := tmpFile.Name()
_ = tmpFile.Close()
defer os.Remove(tmpPath)
if err := privesc.Run(context.Background(), sudoPassword, "cp", "-f", memoryPath, tmpPath); err != nil {
return nil, fmt.Errorf("failed to read greeter memory at %s: %w", memoryPath, err)
}
return os.ReadFile(tmpPath)
}
func SyncGreetdAutoLogin(cacheDir, homeDir string, logFunc func(string), sudoPassword string) error {
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
return err
}
configPath := "/etc/greetd/config.toml"
configContent := ""
if data, readErr := os.ReadFile(configPath); readErr == nil {
configContent = string(data)
} else if !os.IsNotExist(readErr) {
return fmt.Errorf("failed to read greetd config: %w", readErr)
}
if !enabled {
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err))
}
newConfig := upsertInitialSession(configContent, "", "", false)
if newConfig == configContent {
if logFunc != nil {
logFunc("✓ Greeter auto-login disabled")
}
return nil
}
return writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, "✓ Disabled greeter auto-login")
}
if loginUser == "" || sessionExec == "" {
if logFunc != nil {
logFunc("⚠ Greeter auto-login is enabled but user or session is not configured yet. Log in manually once, then run sync.")
}
newConfig := upsertInitialSession(configContent, "", "", false)
if newConfig != configContent {
return writeGreetdConfig(configPath, newConfig, nil, sudoPassword, "")
}
return nil
}
newConfig := upsertInitialSession(configContent, loginUser, sessionExec, true)
if newConfig == configContent {
if logFunc != nil {
logFunc(fmt.Sprintf("✓ Greeter auto-login already configured for %s", loginUser))
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
_ = clearGreeterAutoLoginMemory(memoryPath, sudoPassword)
return nil
}
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Configured greeter auto-login for %s", loginUser)); err != nil {
return err
}
memoryPath := filepath.Join(cacheDir, ".local/state/memory.json")
if err := clearGreeterAutoLoginMemory(memoryPath, sudoPassword); err != nil && logFunc != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to clear greeter auto-login memory flag: %v", err))
}
return nil
}
func SyncGreeterAutoLoginOnly(logFunc func(string), sudoPassword string) error {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
return SyncGreetdAutoLogin(GreeterCacheDir, homeDir, logFunc, sudoPassword)
}
func DetectGreeterUser() string {
passwdData, err := os.ReadFile("/etc/passwd")
if err == nil {
@@ -988,7 +572,6 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
}
runtimeDirs := []string{
filepath.Join(cacheDir, "users"),
filepath.Join(cacheDir, ".local"),
filepath.Join(cacheDir, ".local", "state"),
filepath.Join(cacheDir, ".local", "share"),
@@ -1672,20 +1255,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve syncing user for per-user greeter cache: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, cacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
sudoPassword: sudoPassword,
}); err != nil {
return fmt.Errorf("per-user greeter cache sync failed: %w", err)
}
if err := SyncGreetdAutoLogin(cacheDir, homeDir, logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: greeter auto-login sync failed: %v", err))
}
if strings.ToLower(compositor) != "niri" {
return nil
}
@@ -2150,22 +1719,29 @@ vt = 1
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
enabled, loginUser, sessionExec, resolveErr := resolveGreeterAutoLoginState(GreeterCacheDir, homeDir)
if resolveErr != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to resolve greeter auto-login state: %v", resolveErr))
} else if enabled && loginUser != "" && sessionExec != "" {
newConfig = upsertInitialSession(newConfig, loginUser, sessionExec, true)
} else {
newConfig = upsertInitialSession(newConfig, "", "", false)
}
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
if err != nil {
return fmt.Errorf("failed to create temp greetd config: %w", err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(newConfig); err != nil {
_ = tmpFile.Close()
return fmt.Errorf("failed to write temp greetd config: %w", err)
}
if err := tmpFile.Close(); err != nil {
return fmt.Errorf("failed to close temp greetd config: %w", err)
}
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)); err != nil {
return err
if err := privesc.Run(context.Background(), sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
return fmt.Errorf("failed to create /etc/greetd: %w", err)
}
if err := privesc.Run(context.Background(), sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
return fmt.Errorf("failed to install greetd config: %w", err)
}
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue))
return nil
}
-145
View File
@@ -3,7 +3,6 @@ package greeter
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -97,147 +96,3 @@ func TestResolveGreeterThemeSyncState(t *testing.T) {
})
}
}
func TestUpsertInitialSession(t *testing.T) {
t.Parallel()
baseConfig := `[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command niri"
`
t.Run("inserts initial session", func(t *testing.T) {
t.Parallel()
got := upsertInitialSession(baseConfig, "alice", "niri", true)
if !strings.Contains(got, "[initial_session]") {
t.Fatalf("expected [initial_session] section, got:\n%s", got)
}
if !strings.Contains(got, `user = "alice"`) {
t.Fatalf("expected alice user in initial session, got:\n%s", got)
}
if !strings.Contains(got, `env XDG_SESSION_TYPE=wayland sh -c 'exec niri'`) {
t.Fatalf("expected wrapped session command, got:\n%s", got)
}
})
t.Run("updates existing initial session", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "bob"
command = "old-command"
`
got := upsertInitialSession(existing, "alice", "Hyprland", true)
if strings.Contains(got, `user = "bob"`) {
t.Fatalf("expected bob to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `exec Hyprland`) {
t.Fatalf("expected Hyprland command, got:\n%s", got)
}
})
t.Run("removes initial session when disabled", func(t *testing.T) {
t.Parallel()
existing := baseConfig + `
[initial_session]
user = "alice"
command = "niri"
`
got := upsertInitialSession(existing, "", "", false)
if strings.Contains(got, "[initial_session]") {
t.Fatalf("expected initial session removed, got:\n%s", got)
}
if !strings.Contains(got, "[default_session]") {
t.Fatalf("expected default session preserved, got:\n%s", got)
}
})
}
func TestStripDesktopExecCodes(t *testing.T) {
t.Parallel()
got := stripDesktopExecCodes("niri --session %f")
want := "niri --session"
if got != want {
t.Fatalf("stripDesktopExecCodes = %q, want %q", got, want)
}
}
func TestResolveGreeterAutoLoginState(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": true,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if !enabled || loginUser != "alice" || sessionExec != "niri" {
t.Fatalf("got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
}
}
func TestResolveGreeterAutoLoginStateIgnoresMemoryFlag(t *testing.T) {
t.Parallel()
cacheDir := t.TempDir()
homeDir := t.TempDir()
writeTestFile(t, filepath.Join(cacheDir, "settings.json"), `{
"greeterAutoLogin": false,
"greeterRememberLastUser": true,
"greeterRememberLastSession": true
}`)
writeTestFile(t, filepath.Join(cacheDir, ".local/state/memory.json"), `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice",
"lastSessionExec": "niri"
}`)
enabled, loginUser, sessionExec, err := resolveGreeterAutoLoginState(cacheDir, homeDir)
if err != nil {
t.Fatalf("resolveGreeterAutoLoginState returned error: %v", err)
}
if enabled || loginUser != "" || sessionExec != "" {
t.Fatalf("expected disabled with empty user/exec, got enabled=%v user=%q exec=%q", enabled, loginUser, sessionExec)
}
}
func TestClearGreeterAutoLoginMemory(t *testing.T) {
t.Parallel()
memoryPath := filepath.Join(t.TempDir(), "memory.json")
writeTestFile(t, memoryPath, `{
"autoLoginEnabled": true,
"lastSuccessfulUser": "alice"
}`)
if err := clearGreeterAutoLoginMemory(memoryPath, ""); err != nil {
t.Fatalf("clearGreeterAutoLoginMemory returned error: %v", err)
}
data, err := os.ReadFile(memoryPath)
if err != nil {
t.Fatalf("failed to read memory file: %v", err)
}
if strings.Contains(string(data), "autoLoginEnabled") {
t.Fatalf("expected autoLoginEnabled removed, got: %s", string(data))
}
if !strings.Contains(string(data), "lastSuccessfulUser") {
t.Fatalf("expected other memory fields preserved, got: %s", string(data))
}
}
-548
View File
@@ -1,548 +0,0 @@
package greeter
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
var monitorWallpaperSanitizer = regexp.MustCompile(`[^a-zA-Z0-9]+`)
func userGreeterCacheDir(cacheDir, username string) string {
return filepath.Join(cacheDir, "users", username)
}
func isUserOwnedGreeterCacheSlot(path, username string) bool {
if strings.TrimSpace(username) == "" {
return false
}
userDir, err := filepath.Abs(userGreeterCacheDir(GreeterCacheDir, username))
if err != nil {
return false
}
abs, err := filepath.Abs(path)
if err != nil {
return false
}
return abs == userDir || strings.HasPrefix(abs, userDir+string(filepath.Separator))
}
func UserIsInGreeterGroup(username string) bool {
group := DetectGreeterGroup()
if !utils.HasGroup(group) {
return false
}
groupsCmd := exec.Command("groups", username)
groupsOutput, err := groupsCmd.Output()
if err != nil {
return false
}
return strings.Contains(string(groupsOutput), group)
}
func CanSyncOwnUserGreeterProfile(username string) bool {
currentUser, err := user.Current()
if err != nil || currentUser.Username != username {
return false
}
if !UserIsInGreeterGroup(username) {
return false
}
usersDir := filepath.Join(GreeterCacheDir, "users")
if st, err := os.Stat(usersDir); err != nil || !st.IsDir() {
return false
}
testFile := filepath.Join(usersDir, ".write-test-"+username)
file, err := os.OpenFile(testFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
return false
}
_ = file.Close()
_ = os.Remove(testFile)
return true
}
func GreeterProfileSyncReady() bool {
if command := readGreeterSessionCommand(); command != "" && strings.Contains(command, "dms-greeter") {
return true
}
usersDir := filepath.Join(GreeterCacheDir, "users")
st, err := os.Stat(usersDir)
return err == nil && st.IsDir()
}
func readGreeterSessionCommand() string {
data, err := os.ReadFile("/etc/greetd/config.toml")
if err != nil {
return ""
}
inDefaultSession := false
for line := range strings.SplitSeq(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
inDefaultSession = strings.EqualFold(strings.Trim(trimmed, "[]"), "default_session")
continue
}
if !inDefaultSession {
continue
}
if idx := strings.Index(trimmed, "#"); idx >= 0 {
trimmed = strings.TrimSpace(trimmed[:idx])
}
if !strings.HasPrefix(trimmed, "command") {
continue
}
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) != 2 {
continue
}
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
if command != "" {
return command
}
}
return ""
}
// SyncUserProfileCache writes the current user's theme slot under users/<username>/
// without modifying greetd or other system configuration. Requires membership in the
// greeter group and a prior full greeter setup by an administrator.
func SyncUserProfileCache(logFunc func(string)) error {
if logFunc == nil {
logFunc = func(string) {}
}
if !GreeterProfileSyncReady() {
return fmt.Errorf("greeter is not set up on this system yet; an administrator must run 'dms greeter install' or 'dms greeter sync' once first")
}
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to resolve current user: %w", err)
}
if !CanSyncOwnUserGreeterProfile(currentUser.Username) {
group := DetectGreeterGroup()
return fmt.Errorf("cannot sync greeter profile: you must be in the %s group with write access to %s/users\nAsk an administrator to run:\n sudo usermod -aG %s %s\nThen log out and back in before running:\n dms greeter sync --profile",
group, GreeterCacheDir, group, currentUser.Username)
}
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
return fmt.Errorf("failed to resolve greeter color source: %w", err)
}
if err := syncUserGreeterCacheSlot(homeDir, GreeterCacheDir, currentUser.Username, state, logFunc, userSlotSyncOpts{
profileOnly: true,
}); err != nil {
return err
}
logFunc(fmt.Sprintf(" → %s/users/%s/", GreeterCacheDir, currentUser.Username))
return nil
}
func canWriteUserGreeterCacheSlot(dest, username string) bool {
return isUserOwnedGreeterCacheSlot(dest, username) && CanSyncOwnUserGreeterProfile(username)
}
type userSlotSyncOpts struct {
sudoPassword string
profileOnly bool
username string
}
func (o userSlotSyncOpts) useDirectWrite(dest string) bool {
if !o.profileOnly {
return false
}
return canWriteUserGreeterCacheSlot(dest, o.username)
}
func isGreeterCachePath(path string) bool {
abs, err := filepath.Abs(path)
if err != nil {
return true
}
cacheAbs, err := filepath.Abs(GreeterCacheDir)
if err != nil {
return true
}
if abs == cacheAbs {
return true
}
return strings.HasPrefix(abs, cacheAbs+string(filepath.Separator))
}
func greeterCacheOwner() string {
greeterGroup := DetectGreeterGroup()
daemonUser := DetectGreeterUser()
return daemonUser + ":" + greeterGroup
}
func ensureGreeterCacheSubdir(dir string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dir) {
if err := os.MkdirAll(dir, 0o770); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
return nil
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "mkdir", "-p", dir); err != nil {
return fmt.Errorf("failed to create cache directory %s: %w", dir, err)
}
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), opts.sudoPassword, "chown", owner, dir); err != nil {
if fallbackErr := privesc.Run(context.Background(), opts.sudoPassword, "chown", "root:"+DetectGreeterGroup(), dir); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", dir, err)
}
}
if err := privesc.Run(context.Background(), opts.sudoPassword, "chmod", "2770", dir); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dir, err)
}
return nil
}
func setGreeterCacheFileOwnership(path, sudoPassword string) error {
owner := greeterCacheOwner()
if err := privesc.Run(context.Background(), sudoPassword, "chown", owner, path); err != nil {
if fallbackErr := privesc.Run(context.Background(), sudoPassword, "chown", "root:"+DetectGreeterGroup(), path); fallbackErr != nil {
return fmt.Errorf("failed to set ownership on %s: %w", path, err)
}
}
if err := privesc.Run(context.Background(), sudoPassword, "chmod", "644", path); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", path, err)
}
return nil
}
func syncUserGreeterCacheSlot(homeDir, cacheDir, username string, state greeterThemeSyncState, logFunc func(string), opts userSlotSyncOpts) error {
if strings.TrimSpace(username) == "" {
return nil
}
opts.username = username
userDir := userGreeterCacheDir(cacheDir, username)
if err := ensureGreeterCacheSubdir(userDir, opts); err != nil {
return err
}
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
settingsBytes, err := os.ReadFile(settingsPath)
if err != nil {
return fmt.Errorf("failed to read settings for user cache slot: %w", err)
}
settingsMap := map[string]any{}
if strings.TrimSpace(string(settingsBytes)) != "" {
if err := json.Unmarshal(settingsBytes, &settingsMap); err != nil {
return fmt.Errorf("failed to parse settings for user cache slot: %w", err)
}
}
if customTheme, ok := settingsMap["customThemeFile"].(string); ok && strings.TrimSpace(customTheme) != "" {
resolvedTheme := customTheme
if !filepath.IsAbs(resolvedTheme) {
resolvedTheme = filepath.Join(homeDir, resolvedTheme)
}
if st, statErr := os.Stat(resolvedTheme); statErr == nil && !st.IsDir() {
destTheme := filepath.Join(userDir, "custom-theme.json")
if err := copyFileWithPrivesc(resolvedTheme, destTheme, opts); err != nil {
return err
}
settingsMap["customThemeFile"] = destTheme
}
}
settingsBytes, err = json.Marshal(settingsMap)
if err != nil {
return fmt.Errorf("failed to marshal settings for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "settings.json"), settingsBytes, opts); err != nil {
return err
}
sessionPath := filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json")
sessionBytes, err := os.ReadFile(sessionPath)
if err != nil {
return fmt.Errorf("failed to read session for user cache slot: %w", err)
}
sessionMap := map[string]any{}
if strings.TrimSpace(string(sessionBytes)) != "" {
if err := json.Unmarshal(sessionBytes, &sessionMap); err != nil {
return fmt.Errorf("failed to parse session for user cache slot: %w", err)
}
}
if err := localizeSessionWallpapers(sessionMap, userDir, opts); err != nil {
return err
}
sessionBytes, err = json.Marshal(sessionMap)
if err != nil {
return fmt.Errorf("failed to marshal session for user cache slot: %w", err)
}
if err := writeFileWithPrivesc(filepath.Join(userDir, "session.json"), sessionBytes, opts); err != nil {
return err
}
colorsSource := state.effectiveColorsSource(homeDir)
if err := copyFileWithPrivesc(colorsSource, filepath.Join(userDir, "colors.json"), opts); err != nil {
return fmt.Errorf("failed to copy colors for user cache slot: %w", err)
}
if err := syncUserProfileImage(homeDir, userDir, opts); err != nil {
return err
}
rootOverride := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
userOverride := filepath.Join(userDir, "greeter_wallpaper_override.jpg")
if st, statErr := os.Stat(rootOverride); statErr == nil && !st.IsDir() {
if err := copyFileWithPrivesc(rootOverride, userOverride, opts); err != nil {
return fmt.Errorf("failed to copy greeter wallpaper override for user cache slot: %w", err)
}
} else if opts.useDirectWrite(userOverride) {
_ = os.Remove(userOverride)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", userOverride)
}
logFunc(fmt.Sprintf("✓ Synced per-user greeter cache for %s", username))
return nil
}
func localizeSessionWallpapers(session map[string]any, userDir string, opts userSlotSyncOpts) error {
stringKeys := []struct {
key string
prefix string
}{
{"wallpaperPath", "wallpaper"},
{"wallpaperPathLight", "wallpaper-light"},
{"wallpaperPathDark", "wallpaper-dark"},
}
for _, item := range stringKeys {
if err := localizeWallpaperStringField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
mapKeys := []struct {
key string
prefix string
}{
{"monitorWallpapers", "wallpaper-monitor"},
{"monitorWallpapersLight", "wallpaper-monitor-light"},
{"monitorWallpapersDark", "wallpaper-monitor-dark"},
}
for _, item := range mapKeys {
if err := localizeWallpaperMapField(session, item.key, userDir, item.prefix, opts); err != nil {
return err
}
}
return nil
}
func localizeWallpaperStringField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok {
return nil
}
path, ok := raw.(string)
if !ok || strings.TrimSpace(path) == "" {
return nil
}
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix, opts)
if err != nil {
return err
}
if dest != "" {
session[key] = dest
}
return nil
}
func localizeWallpaperMapField(session map[string]any, key, userDir, prefix string, opts userSlotSyncOpts) error {
raw, ok := session[key]
if !ok || raw == nil {
return nil
}
values, ok := raw.(map[string]any)
if !ok {
return nil
}
for monitor, rawPath := range values {
path, ok := rawPath.(string)
if !ok || strings.TrimSpace(path) == "" {
continue
}
safeMonitor := monitorWallpaperSanitizer.ReplaceAllString(monitor, "-")
dest, err := copyWallpaperIntoUserCache(path, userDir, prefix+"-"+safeMonitor, opts)
if err != nil {
return err
}
if dest != "" {
values[monitor] = dest
}
}
return nil
}
func copyWallpaperIntoUserCache(srcPath, userDir, prefix string, opts userSlotSyncOpts) (string, error) {
if strings.TrimSpace(srcPath) == "" {
return "", nil
}
st, err := os.Stat(srcPath)
if err != nil || st.IsDir() {
return "", nil
}
ext := filepath.Ext(srcPath)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, prefix+ext)
if err := copyFileWithPrivesc(srcPath, dest, opts); err != nil {
return "", err
}
return dest, nil
}
func copyFileWithPrivesc(src, dest string, opts userSlotSyncOpts) error {
if opts.useDirectWrite(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
if !isGreeterCachePath(dest) {
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", dest, err)
}
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("failed to read %s: %w", src, err)
}
if err := os.WriteFile(dest, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
}
return nil
}
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", dest)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", src, dest); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src, dest, err)
}
return setGreeterCacheFileOwnership(dest, opts.sudoPassword)
}
func writeFileWithPrivesc(path string, data []byte, opts userSlotSyncOpts) error {
if opts.useDirectWrite(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o770); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
if !isGreeterCachePath(path) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create parent dir for %s: %w", path, err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return fmt.Errorf("failed to write %s: %w", path, err)
}
return nil
}
tmp, err := os.CreateTemp("", "dms-greeter-user-cache-*")
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %w", path, err)
}
tmpPath := tmp.Name()
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file for %s: %w", path, err)
}
if err := tmp.Close(); err != nil {
_ = os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file for %s: %w", path, err)
}
defer os.Remove(tmpPath)
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
if err := privesc.Run(context.Background(), opts.sudoPassword, "cp", tmpPath, path); err != nil {
return fmt.Errorf("failed to install %s: %w", path, err)
}
return setGreeterCacheFileOwnership(path, opts.sudoPassword)
}
func resolveUserProfileImageSource(homeDir string) string {
candidates := []string{
filepath.Join(homeDir, ".face"),
filepath.Join(homeDir, ".face.icon"),
}
if homeDir != "" {
username := filepath.Base(homeDir)
if username != "" && username != "." && username != string(filepath.Separator) {
candidates = append([]string{filepath.Join("/var/lib/AccountsService/icons", username)}, candidates...)
}
}
for _, src := range candidates {
st, err := os.Stat(src)
if err == nil && !st.IsDir() && st.Size() > 0 {
return src
}
}
return ""
}
func syncUserProfileImage(homeDir, userDir string, opts userSlotSyncOpts) error {
for _, name := range []string{"profile.jpg", "profile.jpeg", "profile.png", "profile.webp"} {
path := filepath.Join(userDir, name)
if opts.useDirectWrite(path) {
_ = os.Remove(path)
} else {
_ = privesc.Run(context.Background(), opts.sudoPassword, "rm", "-f", path)
}
}
src := resolveUserProfileImageSource(homeDir)
if src == "" {
return nil
}
ext := filepath.Ext(src)
if ext == "" {
ext = ".jpg"
}
dest := filepath.Join(userDir, "profile"+ext)
if err := copyFileWithPrivesc(src, dest, opts); err != nil {
return fmt.Errorf("failed to copy profile image for user cache slot: %w", err)
}
return nil
}
@@ -1,81 +0,0 @@
package greeter
import (
"path/filepath"
"testing"
)
func TestUserGreeterCacheDir(t *testing.T) {
t.Parallel()
got := userGreeterCacheDir("/var/cache/dms-greeter", "alice")
want := filepath.Join("/var/cache/dms-greeter", "users", "alice")
if got != want {
t.Fatalf("userGreeterCacheDir() = %q, want %q", got, want)
}
}
func TestResolveUserProfileImageSource(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
facePath := filepath.Join(homeDir, ".face")
writeTestFile(t, facePath, "face")
got := resolveUserProfileImageSource(homeDir)
if got != facePath {
t.Fatalf("resolveUserProfileImageSource() = %q, want %q", got, facePath)
}
}
func TestIsUserOwnedGreeterCacheSlot(t *testing.T) {
t.Parallel()
slot := filepath.Join(GreeterCacheDir, "users", "alice", "settings.json")
if !isUserOwnedGreeterCacheSlot(slot, "alice") {
t.Fatalf("expected alice to own %q", slot)
}
if isUserOwnedGreeterCacheSlot(slot, "bob") {
t.Fatalf("expected bob not to own alice slot")
}
if isUserOwnedGreeterCacheSlot(filepath.Join(GreeterCacheDir, "settings.json"), "alice") {
t.Fatalf("expected root cache file not to be a user slot")
}
}
func TestLocalizeSessionWallpapers(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
userDir := filepath.Join(homeDir, "users", "alice")
wallpaperPath := filepath.Join(homeDir, "wall.jpg")
writeTestFile(t, wallpaperPath, "wallpaper")
session := map[string]any{
"wallpaperPath": wallpaperPath,
"monitorWallpapers": map[string]any{
"DP-1": wallpaperPath,
},
}
if err := localizeSessionWallpapers(session, userDir, userSlotSyncOpts{}); err != nil {
t.Fatalf("localizeSessionWallpapers returned error: %v", err)
}
gotPath, ok := session["wallpaperPath"].(string)
if !ok || gotPath == "" {
t.Fatalf("expected localized wallpaperPath, got %#v", session["wallpaperPath"])
}
if gotPath == wallpaperPath {
t.Fatalf("expected copied wallpaper path, still points to source")
}
monitorMap, ok := session["monitorWallpapers"].(map[string]any)
if !ok {
t.Fatalf("expected monitorWallpapers map")
}
monitorPath, ok := monitorMap["DP-1"].(string)
if !ok || monitorPath == "" || monitorPath == wallpaperPath {
t.Fatalf("expected localized monitor wallpaper, got %#v", monitorMap["DP-1"])
}
}
+29 -684
View File
@@ -68,8 +68,6 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
ConfigFormat: result.DMSStatus.ConfigFormat,
ReadOnly: result.DMSStatus.ReadOnly,
}
}
@@ -221,9 +219,6 @@ func (h *HyprlandProvider) validateAction(action string) error {
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
if err := h.validateAction(action); err != nil {
return err
}
@@ -247,10 +242,9 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
}
}
canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey)
normalizedKey := strings.ToLower(key)
existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: canonicalKey,
Key: key,
Action: action,
Description: description,
Flags: flags,
@@ -261,28 +255,21 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
}
func (h *HyprlandProvider) RemoveBind(key string) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
existingBinds, err := h.loadOverrideBinds()
if err != nil {
return nil
}
canonicalKey := canonicalHyprlandOverrideKey(key)
normalizedKey := hyprlandOverrideMapKey(canonicalKey)
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: canonicalKey, Unbind: true}
normalizedKey := strings.ToLower(key)
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: key, Unbind: true}
return h.writeOverrideBinds(existingBinds)
}
func (h *HyprlandProvider) ResetBind(key string) error {
if err := h.ensureWritableConfig(); err != nil {
return err
}
existingBinds, err := h.loadOverrideBinds()
if err != nil {
return nil
}
normalizedKey := hyprlandOverrideMapKey(key)
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return h.writeOverrideBinds(existingBinds)
}
@@ -297,46 +284,10 @@ type hyprlandOverrideBind struct {
Unbind bool
}
func (h *HyprlandProvider) ensureWritableConfig() error {
if h.isLegacyConfigReadOnly() {
return fmt.Errorf("hyprland legacy conf configs are read-only; run dms setup to migrate to Lua before editing keybinds")
}
return nil
}
func (h *HyprlandProvider) isLegacyConfigReadOnly() bool {
expanded, err := utils.ExpandPath(h.configPath)
if err != nil {
expanded = h.configPath
}
luaPath := filepath.Join(expanded, "hyprland.lua")
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
return false
}
confPath := filepath.Join(expanded, "hyprland.conf")
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
return true
}
return false
}
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
return readLuaOrHyprlangOverride(h.GetOverridePath())
}
func canonicalHyprlandOverrideKey(key string) string {
trimmed := strings.TrimSpace(key)
normalized := luaKeyComboToInternalKey(trimmed)
if normalized == "" {
return trimmed
}
return normalized
}
func hyprlandOverrideMapKey(key string) string {
return strings.ToLower(canonicalHyprlandOverrideKey(key))
}
func (h *HyprlandProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
@@ -417,629 +368,24 @@ func normalizeLuaBindKeyPart(part string) string {
return part
}
type luaField struct {
name string
value string
}
func luaDispatcherTableCall(funcName string, fields ...luaField) string {
parts := make([]string, 0, len(fields))
for _, field := range fields {
if field.name == "" || field.value == "" {
continue
}
parts = append(parts, field.name+" = "+field.value)
}
return fmt.Sprintf(`%s({ %s })`, funcName, strings.Join(parts, ", "))
}
func luaStringField(name, value string) luaField {
return luaField{name: name, value: strconv.Quote(strings.TrimSpace(value))}
}
func luaBoolField(name string, value bool) luaField {
if value {
return luaField{name: name, value: "true"}
}
return luaField{name: name, value: "false"}
}
func luaNumberOrStringField(name, value string) luaField {
value = strings.TrimSpace(value)
if isBareLuaNumber(value) {
return luaField{name: name, value: value}
}
return luaStringField(name, value)
}
func isBareLuaNumber(value string) bool {
if value == "" || strings.HasPrefix(value, "+") {
return false
}
if value[0] == '-' {
value = value[1:]
}
if value == "" {
return false
}
digitsBeforeDot := 0
i := 0
for i < len(value) && value[i] >= '0' && value[i] <= '9' {
digitsBeforeDot++
i++
}
digitsAfterDot := 0
if i < len(value) && value[i] == '.' {
i++
for i < len(value) && value[i] >= '0' && value[i] <= '9' {
digitsAfterDot++
i++
}
}
return i == len(value) && (digitsBeforeDot > 0 || digitsAfterDot > 0)
}
func splitHyprlandAction(action string) (dispatcher, params string) {
action = strings.TrimSpace(action)
if action == "" {
return "", ""
}
idx := strings.IndexFunc(action, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
})
if idx < 0 {
return strings.ToLower(action), ""
}
return strings.ToLower(strings.TrimSpace(action[:idx])), strings.TrimSpace(action[idx+1:])
}
func isKnownHyprlandDispatcher(dispatcher string) bool {
switch dispatcher {
case "exec", "execr", "spawn",
"killactive", "forcekillactive", "closewindow", "killwindow",
"signal", "signalwindow", "togglefloating", "setfloating", "settiled",
"workspace", "renameworkspace", "fullscreen", "fullscreenstate", "fakefullscreen",
"movetoworkspace", "movetoworkspacesilent", "pseudo", "movefocus",
"movewindow", "swapwindow", "centerwindow", "togglegroup", "changegroupactive",
"movegroupwindow", "focusmonitor", "movecursortocorner", "movecursor",
"workspaceopt", "exit", "movecurrentworkspacetomonitor", "focusworkspaceoncurrentmonitor",
"moveworkspacetomonitor", "togglespecialworkspace", "forcerendererreload",
"resizeactive", "moveactive", "cyclenext", "focuswindowbyclass", "focuswindow",
"tagwindow", "toggleswallow", "submap", "pass", "sendshortcut", "sendkeystate",
"layoutmsg", "splitratio", "dpms", "movewindowpixel", "resizewindowpixel",
"swapnext", "swapactiveworkspaces", "pin", "mouse", "bringactivetotop",
"alterzorder", "focusurgentorlast", "focuscurrentorlast", "lockgroups",
"lockactivegroup", "moveintogroup", "moveoutofgroup", "movewindoworgroup",
"moveintoorcreategroup", "setignoregrouplock", "denywindowfromgroup", "event",
"global", "setprop", "forceidle":
return true
default:
return false
}
}
func firstParam(params string) (head, rest string) {
params = strings.TrimSpace(params)
if params == "" {
return "", ""
}
fields := strings.Fields(params)
if len(fields) == 0 {
return "", ""
}
head = fields[0]
rest = strings.TrimSpace(strings.TrimPrefix(params, head))
return head, rest
}
func xyParams(params string) (x, y string, relative bool, ok bool) {
fields := strings.Fields(params)
if len(fields) > 0 && strings.EqualFold(fields[0], "exact") {
relative = false
fields = fields[1:]
} else {
relative = true
}
if len(fields) < 2 {
return "", "", relative, false
}
return fields[0], fields[1], relative, true
}
func dispatcherWorkspaceMove(params string, follow *bool) string {
workspace, window := firstParam(params)
if workspace == "" {
return ""
}
fields := []luaField{luaStringField("workspace", workspace)}
if follow != nil {
fields = append(fields, luaBoolField("follow", *follow))
}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.move", fields...)
}
func dispatcherActiveMoveResize(funcName, params string) string {
x, y, relative, ok := xyParams(params)
if !ok {
return ""
}
if !isBareLuaNumber(x) || !isBareLuaNumber(y) {
return ""
}
return luaDispatcherTableCall(funcName,
luaNumberOrStringField("x", x),
luaNumberOrStringField("y", y),
luaBoolField("relative", relative),
)
}
func dispatcherWindowMoveResize(funcName, params string) string {
geometry, window := splitCommaParams(params)
x, y, relative, ok := xyParams(geometry)
if !ok {
return ""
}
if !isBareLuaNumber(x) || !isBareLuaNumber(y) {
return ""
}
fields := []luaField{
luaNumberOrStringField("x", x),
luaNumberOrStringField("y", y),
luaBoolField("relative", relative),
}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall(funcName, fields...)
}
func splitCommaParams(params string) (left, right string) {
left = strings.TrimSpace(params)
if idx := strings.Index(left, ","); idx >= 0 {
right = strings.TrimSpace(left[idx+1:])
left = strings.TrimSpace(left[:idx])
}
return left, right
}
func luaHyprctlDispatchFunction(action string) string {
return fmt.Sprintf(`function() hl.exec_cmd(%s) end`, strconv.Quote("hyprctl dispatch "+strings.TrimSpace(action)))
}
func luaToggleActionValue(params string) string {
switch strings.ToLower(strings.TrimSpace(params)) {
case "on", "enable", "enabled", "set", "lock":
return "on"
case "off", "disable", "disabled", "unset", "unlock":
return "off"
default:
return "toggle"
}
}
func dispatcherToggleTableCall(funcName, params string) string {
return luaDispatcherTableCall(funcName, luaStringField("action", luaToggleActionValue(params)))
}
func dispatcherCycleNext(params string) string {
params = strings.TrimSpace(strings.ToLower(params))
if params == "" {
return `hl.dsp.window.cycle_next()`
}
fields := []luaField{}
for _, field := range strings.Fields(params) {
switch field {
case "prev", "previous", "b":
fields = append(fields, luaBoolField("next", false))
case "next", "f":
fields = append(fields, luaBoolField("next", true))
case "tiled":
fields = append(fields, luaBoolField("tiled", true))
case "floating":
fields = append(fields, luaBoolField("floating", true))
}
}
if len(fields) == 0 {
return ""
}
return luaDispatcherTableCall("hl.dsp.window.cycle_next", fields...)
}
func dispatcherSwapNext(params string) string {
switch strings.ToLower(strings.TrimSpace(params)) {
case "prev", "previous", "b":
return `hl.dsp.window.swap({ prev = true })`
default:
return `hl.dsp.window.swap({ next = true })`
}
}
func dispatcherGroupActive(params string) string {
switch strings.ToLower(strings.TrimSpace(params)) {
case "f", "next", "forward":
return `hl.dsp.group.next()`
case "b", "prev", "previous", "backward":
return `hl.dsp.group.prev()`
}
if isBareLuaNumber(params) {
return luaDispatcherTableCall("hl.dsp.group.active", luaNumberOrStringField("index", params))
}
return ""
}
func dispatcherMoveGroupWindow(params string) string {
switch strings.ToLower(strings.TrimSpace(params)) {
case "b", "prev", "previous", "backward":
return `hl.dsp.group.move_window({ forward = false })`
default:
return `hl.dsp.group.move_window({ forward = true })`
}
}
func dispatcherCursorMove(params string) string {
x, y, _, ok := xyParams(params)
if !ok || !isBareLuaNumber(x) || !isBareLuaNumber(y) {
return ""
}
return luaDispatcherTableCall("hl.dsp.cursor.move", luaNumberOrStringField("x", x), luaNumberOrStringField("y", y))
}
func dispatcherSignal(params string) string {
signal, window := firstParam(params)
if signal == "" || !isBareLuaNumber(signal) {
return ""
}
fields := []luaField{luaNumberOrStringField("signal", signal)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.signal", fields...)
}
func dispatcherSignalWindow(params string) string {
window, rest := firstParam(params)
signal, _ := firstParam(rest)
if signal == "" || !isBareLuaNumber(signal) {
return ""
}
fields := []luaField{luaNumberOrStringField("signal", signal)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.signal", fields...)
}
func dispatcherTagWindow(params string) string {
tag, window := firstParam(params)
if tag == "" {
return ""
}
fields := []luaField{luaStringField("tag", tag)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.tag", fields...)
}
func luaActionStringFromKnownHyprlandAction(action string) (string, bool) {
dispatcher, params := splitHyprlandAction(action)
switch dispatcher {
case "spawn", "exec":
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(params)), true
case "execr":
return fmt.Sprintf(`hl.dsp.exec_raw(%s)`, strconv.Quote(params)), true
case "killactive":
return `hl.dsp.window.close()`, true
case "forcekillactive":
return `hl.dsp.window.kill()`, true
case "closewindow":
if params == "" {
return `hl.dsp.window.close()`, true
}
return luaDispatcherTableCall("hl.dsp.window.close", luaStringField("window", params)), true
case "killwindow":
if params == "" {
return `hl.dsp.window.kill()`, true
}
return luaDispatcherTableCall("hl.dsp.window.kill", luaStringField("window", params)), true
case "togglefloating":
return dispatcherToggleTableCall("hl.dsp.window.float", "toggle"), true
case "setfloating":
return dispatcherToggleTableCall("hl.dsp.window.float", "on"), true
case "settiled":
return dispatcherToggleTableCall("hl.dsp.window.float", "off"), true
case "fullscreen":
mode := strings.TrimSpace(params)
switch mode {
case "", "0":
return `hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })`, true
case "1":
return `hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, true
}
return luaHyprctlDispatchFunction(action), true
case "fullscreenstate":
internal, rest := firstParam(params)
client, _ := firstParam(rest)
if internal != "" && client != "" {
return luaDispatcherTableCall("hl.dsp.window.fullscreen_state",
luaNumberOrStringField("internal", internal),
luaNumberOrStringField("client", client),
), true
}
case "fakefullscreen":
return luaHyprctlDispatchFunction(action), true
case "pin":
if params == "" {
return `hl.dsp.window.pin()`, true
}
return dispatcherToggleTableCall("hl.dsp.window.pin", params), true
case "pseudo":
return dispatcherToggleTableCall("hl.dsp.window.pseudo", params), true
case "centerwindow":
return `hl.dsp.window.center()`, true
case "resizewindow":
return `hl.dsp.window.resize()`, true
case "movewindow":
if params == "" {
return `hl.dsp.window.drag()`, true
}
if monitor, ok := strings.CutPrefix(params, "mon:"); ok {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("monitor", monitor)), true
}
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("direction", params)), true
case "swapwindow":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.window.swap", luaStringField("direction", params)), true
case "swapnext":
return dispatcherSwapNext(params), true
case "resizeactive":
if expr := dispatcherActiveMoveResize("hl.dsp.window.resize", params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "moveactive":
if expr := dispatcherActiveMoveResize("hl.dsp.window.move", params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "resizewindowpixel":
if expr := dispatcherWindowMoveResize("hl.dsp.window.resize", params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "movewindowpixel":
if expr := dispatcherWindowMoveResize("hl.dsp.window.move", params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "workspace":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("workspace", params)), true
case "focusworkspaceoncurrentmonitor":
if params == "" {
return "", false
}
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("workspace", params), luaBoolField("on_current_monitor", true)), true
case "movetoworkspace":
if expr := dispatcherWorkspaceMove(params, nil); expr != "" {
return expr, true
}
case "movetoworkspacesilent":
follow := false
if expr := dispatcherWorkspaceMove(params, &follow); expr != "" {
return expr, true
}
case "togglespecialworkspace":
if params == "" {
return `hl.dsp.workspace.toggle_special()`, true
}
return fmt.Sprintf(`hl.dsp.workspace.toggle_special(%s)`, strconv.Quote(params)), true
case "renameworkspace":
workspace, name := firstParam(params)
if workspace != "" {
fields := []luaField{luaStringField("workspace", workspace)}
if name != "" {
fields = append(fields, luaStringField("name", name))
}
return luaDispatcherTableCall("hl.dsp.workspace.rename", fields...), true
}
case "movecurrentworkspacetomonitor":
if params != "" {
return luaDispatcherTableCall("hl.dsp.workspace.move", luaStringField("monitor", params)), true
}
case "moveworkspacetomonitor":
workspace, monitor := firstParam(params)
if workspace != "" && monitor != "" {
return luaDispatcherTableCall("hl.dsp.workspace.move", luaStringField("workspace", workspace), luaStringField("monitor", monitor)), true
}
case "workspaceopt":
return luaHyprctlDispatchFunction(action), true
case "swapactiveworkspaces":
monitor1, rest := firstParam(params)
monitor2, _ := firstParam(rest)
if monitor1 != "" && monitor2 != "" {
return luaDispatcherTableCall("hl.dsp.workspace.swap_monitors", luaStringField("monitor1", monitor1), luaStringField("monitor2", monitor2)), true
}
case "movefocus":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("direction", params)), true
}
case "focusmonitor":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("monitor", params)), true
}
case "focuswindow":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("window", params)), true
}
case "focuswindowbyclass":
if params != "" {
return luaDispatcherTableCall("hl.dsp.focus", luaStringField("window", "class:"+params)), true
}
case "focuscurrentorlast":
return `hl.dsp.focus({ last = true })`, true
case "focusurgentorlast":
return `hl.dsp.focus({ urgent_or_last = true })`, true
case "cyclenext":
if expr := dispatcherCycleNext(params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "layoutmsg":
if params != "" {
return fmt.Sprintf(`hl.dsp.layout(%s)`, strconv.Quote(params)), true
}
case "splitratio":
return luaHyprctlDispatchFunction(action), true
case "alterzorder":
mode, window := firstParam(params)
if mode != "" {
fields := []luaField{luaStringField("mode", mode)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.window.alter_zorder", fields...), true
}
case "setprop":
window, rest := firstParam(params)
prop, value := firstParam(rest)
if window != "" && prop != "" && value != "" {
return luaDispatcherTableCall("hl.dsp.window.set_prop",
luaStringField("window", window),
luaStringField("prop", prop),
luaStringField("value", value),
), true
}
case "bringactivetotop":
return `hl.dsp.window.bring_to_top()`, true
case "toggleswallow":
return `hl.dsp.window.toggle_swallow()`, true
case "signal":
if expr := dispatcherSignal(params); expr != "" {
return expr, true
}
case "signalwindow":
if expr := dispatcherSignalWindow(params); expr != "" {
return expr, true
}
case "tagwindow":
if expr := dispatcherTagWindow(params); expr != "" {
return expr, true
}
case "dpms":
dpmsAction := strings.TrimSpace(params)
switch dpmsAction {
case "on":
dpmsAction = "enable"
case "off":
dpmsAction = "disable"
}
if dpmsAction == "" {
return `hl.dsp.dpms({})`, true
}
return luaDispatcherTableCall("hl.dsp.dpms", luaStringField("action", dpmsAction)), true
case "exit":
return `hl.dsp.exit()`, true
case "submap":
return fmt.Sprintf(`hl.dsp.submap(%s)`, strconv.Quote(params)), true
case "global":
return fmt.Sprintf(`hl.dsp.global(%s)`, strconv.Quote(params)), true
case "event":
return fmt.Sprintf(`hl.dsp.event(%s)`, strconv.Quote(params)), true
case "pass":
if params == "" {
return `hl.dsp.pass({})`, true
}
return luaDispatcherTableCall("hl.dsp.pass", luaStringField("window", params)), true
case "sendshortcut":
mod, rest := firstParam(params)
key, window := firstParam(rest)
if mod != "" && key != "" {
fields := []luaField{luaStringField("mods", mod), luaStringField("key", key)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.send_shortcut", fields...), true
}
case "sendkeystate":
mod, rest := firstParam(params)
key, rest := firstParam(rest)
state, window := firstParam(rest)
if mod != "" && key != "" && state != "" {
fields := []luaField{luaStringField("mods", mod), luaStringField("key", key), luaStringField("state", state)}
if window != "" {
fields = append(fields, luaStringField("window", window))
}
return luaDispatcherTableCall("hl.dsp.send_key_state", fields...), true
}
case "movecursortocorner":
if params != "" && isBareLuaNumber(params) {
return luaDispatcherTableCall("hl.dsp.cursor.move_to_corner", luaNumberOrStringField("corner", params)), true
}
case "movecursor":
if expr := dispatcherCursorMove(params); expr != "" {
return expr, true
}
case "togglegroup":
return `hl.dsp.group.toggle()`, true
case "changegroupactive":
if expr := dispatcherGroupActive(params); expr != "" {
return expr, true
}
return luaHyprctlDispatchFunction(action), true
case "movegroupwindow":
return dispatcherMoveGroupWindow(params), true
case "moveintogroup":
if params != "" {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("into_group", params)), true
}
case "moveintoorcreategroup":
if params != "" {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("into_or_create_group", params)), true
}
case "moveoutofgroup":
if params != "" {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("out_of_group", params)), true
}
return luaDispatcherTableCall("hl.dsp.window.move", luaBoolField("out_of_group", true)), true
case "movewindoworgroup":
if params != "" {
return luaDispatcherTableCall("hl.dsp.window.move", luaStringField("direction", params), luaBoolField("group_aware", true)), true
}
case "lockgroups":
return dispatcherToggleTableCall("hl.dsp.group.lock", params), true
case "lockactivegroup":
return dispatcherToggleTableCall("hl.dsp.group.lock_active", params), true
case "denywindowfromgroup":
return dispatcherToggleTableCall("hl.dsp.window.deny_from_group", params), true
case "setignoregrouplock":
return luaHyprctlDispatchFunction(action), true
case "forcerendererreload":
return `hl.dsp.force_renderer_reload()`, true
case "forceidle":
if params != "" && isBareLuaNumber(params) {
return fmt.Sprintf(`hl.dsp.force_idle(%s)`, params), true
}
}
if isKnownHyprlandDispatcher(dispatcher) {
return luaHyprctlDispatchFunction(action), true
}
return "", false
}
func luaActionStringFromHyprlangAction(action string) string {
action = strings.TrimSpace(action)
if expr, ok := luaActionStringFromKnownHyprlandAction(action); ok {
return expr
if strings.HasPrefix(action, "spawn ") {
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimSpace(strings.TrimPrefix(action, "spawn "))))
}
if strings.HasPrefix(action, "exec ") {
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote(strings.TrimPrefix(action, "exec ")))
}
switch action {
case "killactive":
return `hl.dsp.window.kill()`
case "togglefloating":
return `hl.dsp.window.float({ action = "toggle" })`
case "exit":
return `hl.dsp.exit()`
default:
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action))
}
return action
}
func luaExprToInternalAction(expr string) string {
@@ -1061,7 +407,7 @@ func luaBindOptions(bind *hyprlandOverrideBind) []string {
if strings.Contains(bind.Flags, "e") {
opts = append(opts, "repeating = true")
}
if bind.Description != "" {
if bind.Description != "" && strings.Contains(bind.Flags, "d") {
opts = append(opts, fmt.Sprintf("description = %s", strconv.Quote(bind.Description)))
}
return opts
@@ -1081,7 +427,11 @@ func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
if len(opts) > 0 {
fmt.Fprintf(sb, `hl.bind("%s", %s, { %s })`, key, expr, strings.Join(opts, ", "))
} else {
fmt.Fprintf(sb, `hl.bind("%s", %s)`, key, expr)
if bind.Description != "" {
fmt.Fprintf(sb, `hl.bind("%s", %s) -- %s`, key, expr, bind.Description)
} else {
fmt.Fprintf(sb, `hl.bind("%s", %s)`, key, expr)
}
}
sb.WriteByte('\n')
}
@@ -1100,9 +450,6 @@ func parseLuaBindOverrideLine(line string) (*hyprlandOverrideBind, bool) {
action := luaExprToInternalAction(actionExpr)
flags := luaBindOptFlags(optSuffix)
description := luaBindOptDescription(optSuffix)
if description == "" {
description = luaLineTrailingComment(line)
}
return &hyprlandOverrideBind{
Key: internalKey,
Action: action,
@@ -1151,12 +498,11 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
continue
}
if key, ok := parseLuaUnbindLine(line); ok {
pendingUnbinds[hyprlandOverrideMapKey(key)] = canonicalHyprlandOverrideKey(key)
pendingUnbinds[strings.ToLower(key)] = key
continue
}
if kb, ok := parseLuaBindOverrideLine(line); ok {
kb.Key = canonicalHyprlandOverrideKey(kb.Key)
normalizedKey := hyprlandOverrideMapKey(kb.Key)
normalizedKey := strings.ToLower(kb.Key)
binds[normalizedKey] = kb
delete(pendingUnbinds, normalizedKey)
continue
@@ -1174,8 +520,7 @@ func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, e
action = kb.Dispatcher + " " + kb.Params
}
flags := kb.Flags
keyStr = canonicalHyprlandOverrideKey(keyStr)
normalizedKey := hyprlandOverrideMapKey(keyStr)
normalizedKey := strings.ToLower(keyStr)
binds[normalizedKey] = &hyprlandOverrideBind{
Key: keyStr,
Action: action,
@@ -54,8 +54,6 @@ type HyprlandParser struct {
dmsProcessed bool
removedKeys map[string]bool // bare hl.unbind targets (negative overrides)
defaultDMSKeys map[string]bool // keys present in dms/binds.{lua,conf}
configFormat string
readOnly bool
}
func NewHyprlandParser(configDir string) *HyprlandParser {
@@ -312,8 +310,6 @@ type HyprlandDMSStatus struct {
Effective bool
OverriddenBy int
StatusMessage string
ConfigFormat string
ReadOnly bool
}
func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
@@ -323,8 +319,6 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
ConfigFormat: p.configFormat,
ReadOnly: p.readOnly,
}
switch {
@@ -404,13 +398,6 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
if err != nil {
return nil, err
}
if strings.EqualFold(filepath.Ext(mainConfig), ".lua") {
p.configFormat = "lua"
p.readOnly = false
} else {
p.configFormat = "hyprlang"
p.readOnly = true
}
section, err := p.parseFileWithSource(mainConfig, "")
if err != nil {
return nil, err
@@ -882,20 +869,23 @@ func parseLuaStringLiteral(line string, i int) (value string, next int, ok bool)
return "", i, false
}
// parseLuaFirstArgExpr parses a single Lua expression starting at i, stopping at
// the next top-level comma. It handles nested calls/tables and inline functions.
// parseLuaFirstArgExpr parses a single Lua expression starting at i, stopping when parentheses
// opened from the first '(' are balanced (handles nested () and {} and double-quoted strings).
func parseLuaFirstArgExpr(line string, start int) (expr string, next int, ok bool) {
start = skipLuaWS(line, start)
if start >= len(line) {
return "", start, false
}
i := start
parenDepth := 0
braceDepth := 0
bracketDepth := 0
functionDepth := 0
// Find first '(' of the call (e.g. hl.dsp.exec_cmd(...)
firstParen := strings.IndexByte(line[start:], '(')
if firstParen < 0 {
return "", start, false
}
i := start + firstParen
depth := 0
inStr := byte(0)
esc := false
exprStart := start
for ; i < len(line); i++ {
c := line[i]
if inStr != 0 {
@@ -912,66 +902,19 @@ func parseLuaFirstArgExpr(line string, start int) (expr string, next int, ok boo
}
continue
}
if c == '[' && i+1 < len(line) && line[i+1] == '[' {
if end := strings.Index(line[i+2:], "]]"); end >= 0 {
i += end + 3
continue
}
return "", start, false
}
if luaWordAt(line, i, "function") {
functionDepth++
i += len("function") - 1
continue
}
if luaWordAt(line, i, "end") && functionDepth > 0 {
functionDepth--
i += len("end") - 1
continue
}
switch c {
case '"', '\'':
inStr = c
case '(':
parenDepth++
depth++
case ')':
if parenDepth > 0 {
parenDepth--
}
case '{':
braceDepth++
case '}':
if braceDepth > 0 {
braceDepth--
}
case '[':
bracketDepth++
case ']':
if bracketDepth > 0 {
bracketDepth--
}
case ',':
if parenDepth == 0 && braceDepth == 0 && bracketDepth == 0 && functionDepth == 0 {
return strings.TrimSpace(line[start:i]), i, true
depth--
if depth == 0 {
return strings.TrimSpace(line[exprStart : i+1]), i + 1, true
}
}
}
expr = strings.TrimSpace(line[start:i])
return expr, i, expr != ""
}
func luaWordAt(line string, idx int, word string) bool {
if idx < 0 || idx+len(word) > len(line) || line[idx:idx+len(word)] != word {
return false
}
before := idx == 0 || !isLuaIdentByte(line[idx-1])
afterIdx := idx + len(word)
after := afterIdx >= len(line) || !isLuaIdentByte(line[afterIdx])
return before && after
}
func isLuaIdentByte(c byte) bool {
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
return "", start, false
}
// parseLuaBindInvocation parses one hl.bind("KEY", expr [, opts]) on a single line.
@@ -1050,39 +993,19 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
if arg != "" {
if u, err := strconv.Unquote(arg); err == nil {
if strings.HasPrefix(u, "hyprctl dispatch ") {
return splitDispatchCommand(strings.TrimSpace(strings.TrimPrefix(u, "hyprctl dispatch ")))
rest := strings.TrimSpace(strings.TrimPrefix(u, "hyprctl dispatch "))
parts := strings.SplitN(rest, " ", 2)
if len(parts) == 1 {
return parts[0], ""
}
return parts[0], parts[1]
}
return "exec", u
}
}
return "exec", strings.TrimSpace(strings.TrimPrefix(expr, "hl.dsp.exec_cmd"))
case strings.HasPrefix(expr, "hl.dsp.exec_raw("):
return "execr", luaCallStringArgValue(expr, "hl.dsp.exec_raw")
case strings.HasPrefix(expr, "hl.dispatch("):
if arg := luaCallStringArgValue(expr, "hl.dispatch"); arg != "" {
return splitDispatchCommand(arg)
}
return "", ""
case strings.Contains(expr, "hl.exec_cmd("):
if arg := luaEmbeddedCallStringArgValue(expr, "hl.exec_cmd"); strings.HasPrefix(arg, "hyprctl dispatch ") {
return splitDispatchCommand(strings.TrimSpace(strings.TrimPrefix(arg, "hyprctl dispatch ")))
}
case strings.HasPrefix(expr, "hl.dsp.window.close("):
if window := luaTableStringField(expr, "window"); window != "" {
return "closewindow", window
}
if arg := luaCallStringArgValue(expr, "hl.dsp.window.close"); arg != "" {
return "closewindow", arg
}
case strings.Contains(expr, "hl.dsp.window.kill()"):
return "killactive", ""
case strings.HasPrefix(expr, "hl.dsp.window.kill("):
if window := luaTableStringField(expr, "window"); window != "" {
return "killwindow", window
}
if arg := luaCallStringArgValue(expr, "hl.dsp.window.kill"); arg != "" {
return "killwindow", arg
}
return "forcekillactive", ""
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen("):
switch luaTableStringField(expr, "mode") {
case "maximized", "maximize":
@@ -1091,55 +1014,10 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
return "fullscreen", "0"
}
return "fullscreen", luaTableStringField(expr, "mode")
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen_state("):
internal := luaStringValue(luaTableScalarField(expr, "internal"))
client := luaStringValue(luaTableScalarField(expr, "client"))
return joinDispatcherParams("fullscreenstate", internal, client)
case strings.HasPrefix(expr, "hl.dsp.window.float("):
switch luaToggleActionToLegacy(luaTableStringField(expr, "action")) {
case "on":
return "setfloating", ""
case "off":
return "settiled", ""
default:
return "togglefloating", ""
}
case strings.HasPrefix(expr, "hl.dsp.window.pseudo("):
action := luaToggleActionToLegacy(luaTableStringField(expr, "action"))
if action == "" || action == "toggle" {
return "pseudo", ""
}
return "pseudo", action
case strings.HasPrefix(expr, "hl.dsp.window.pin("):
if action := luaToggleActionToLegacy(luaTableStringField(expr, "action")); action != "" && action != "toggle" {
return "pin", action
}
return "pin", ""
case strings.Contains(expr, "hl.dsp.window.center()"):
return "centerwindow", ""
case strings.Contains(expr, "hl.dsp.window.bring_to_top()"):
return "bringactivetotop", ""
case strings.Contains(expr, "hl.dsp.window.toggle_swallow()"):
return "toggleswallow", ""
return "togglefloating", ""
case strings.Contains(expr, "hl.dsp.group.toggle()"):
return "togglegroup", ""
case strings.Contains(expr, "hl.dsp.group.next()"):
return "changegroupactive", "f"
case strings.Contains(expr, "hl.dsp.group.prev()"):
return "changegroupactive", "b"
case strings.HasPrefix(expr, "hl.dsp.group.active("):
return "changegroupactive", luaStringValue(luaTableScalarField(expr, "index"))
case strings.HasPrefix(expr, "hl.dsp.group.move_window("):
if forward, ok := luaTableBoolField(expr, "forward"); ok && !forward {
return "movegroupwindow", "b"
}
return "movegroupwindow", "f"
case strings.HasPrefix(expr, "hl.dsp.group.lock_active("):
return "lockactivegroup", luaToggleActionToLockArg(luaTableStringField(expr, "action"))
case strings.HasPrefix(expr, "hl.dsp.group.lock("):
return "lockgroups", luaToggleActionToLockArg(luaTableStringField(expr, "action"))
case strings.HasPrefix(expr, "hl.dsp.window.deny_from_group("):
return "denywindowfromgroup", luaToggleActionToLegacy(luaTableStringField(expr, "action"))
case strings.HasPrefix(expr, "hl.dsp.focus("):
switch {
case luaTableStringField(expr, "direction") != "":
@@ -1147,58 +1025,18 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
case luaTableStringField(expr, "monitor") != "":
return "focusmonitor", luaTableStringField(expr, "monitor")
case luaTableStringField(expr, "workspace") != "":
if luaTableBoolFieldValue(expr, "on_current_monitor") {
return "focusworkspaceoncurrentmonitor", luaTableStringField(expr, "workspace")
}
return "workspace", luaTableStringField(expr, "workspace")
case luaTableStringField(expr, "window") != "":
return "focuswindow", luaTableStringField(expr, "window")
case luaTableBoolFieldValue(expr, "urgent_or_last"):
return "focusurgentorlast", ""
case luaTableBoolFieldValue(expr, "last"):
return "focuscurrentorlast", ""
}
case strings.HasPrefix(expr, "hl.dsp.window.move("):
switch {
case luaTableScalarField(expr, "x") != "" || luaTableScalarField(expr, "y") != "":
x := luaStringValue(luaTableScalarField(expr, "x"))
y := luaStringValue(luaTableScalarField(expr, "y"))
if x == "" {
x = "0"
}
if y == "" {
y = "0"
}
prefix := ""
if raw, ok := luaTableBoolField(expr, "relative"); ok && !raw {
prefix = "exact "
}
params := prefix + x + " " + y
if window := luaTableStringField(expr, "window"); window != "" {
return "movewindowpixel", params + "," + window
}
return "moveactive", params
case luaTableStringField(expr, "into_group") != "":
return "moveintogroup", luaTableStringField(expr, "into_group")
case luaTableStringField(expr, "into_or_create_group") != "":
return "moveintoorcreategroup", luaTableStringField(expr, "into_or_create_group")
case luaTableBoolFieldValue(expr, "out_of_group"):
return "moveoutofgroup", ""
case luaTableStringField(expr, "out_of_group") != "":
return "moveoutofgroup", luaTableStringField(expr, "out_of_group")
case luaTableStringField(expr, "direction") != "":
if luaTableBoolFieldValue(expr, "group_aware") {
return "movewindoworgroup", luaTableStringField(expr, "direction")
}
return "movewindow", luaTableStringField(expr, "direction")
case luaTableStringField(expr, "monitor") != "":
return "movewindow", "mon:" + luaTableStringField(expr, "monitor")
case luaTableStringField(expr, "workspace") != "":
action := "movetoworkspace"
if follow, ok := luaTableBoolField(expr, "follow"); ok && !follow {
action = "movetoworkspacesilent"
}
return joinDispatcherParams(action, luaTableStringField(expr, "workspace"), luaTableStringField(expr, "window"))
return "movetoworkspace", luaTableStringField(expr, "workspace")
}
case expr == "hl.dsp.window.drag()":
return "movewindow", ""
@@ -1214,184 +1052,25 @@ func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
if y == "" {
y = "0"
}
prefix := ""
if relative, ok := luaTableBoolField(expr, "relative"); ok && !relative {
prefix = "exact "
}
params := prefix + x + " " + y
if window := luaTableStringField(expr, "window"); window != "" {
return "resizewindowpixel", params + "," + window
}
return "resizeactive", params
return "resizeactive", x + " " + y
}
case strings.HasPrefix(expr, "hl.dsp.window.swap("):
switch {
case luaTableBoolFieldValue(expr, "next"):
return "swapnext", ""
case luaTableBoolFieldValue(expr, "prev"):
return "swapnext", "prev"
}
return "swapwindow", luaTableStringField(expr, "direction")
case strings.HasPrefix(expr, "hl.dsp.window.cycle_next("):
parts := []string{}
if next, ok := luaTableBoolField(expr, "next"); ok && !next {
parts = append(parts, "prev")
}
if luaTableBoolFieldValue(expr, "tiled") {
parts = append(parts, "tiled")
}
if luaTableBoolFieldValue(expr, "floating") {
parts = append(parts, "floating")
}
return "cyclenext", strings.Join(parts, " ")
case strings.HasPrefix(expr, "hl.dsp.window.signal("):
signal := luaStringValue(luaTableScalarField(expr, "signal"))
window := luaTableStringField(expr, "window")
if window != "" {
return joinDispatcherParams("signalwindow", window, signal)
}
return "signal", signal
case strings.HasPrefix(expr, "hl.dsp.window.tag("):
return joinDispatcherParams("tagwindow", luaTableStringField(expr, "tag"), luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.window.alter_zorder("):
mode := luaTableStringField(expr, "mode")
if mode == "" {
mode = luaTableStringField(expr, "zheight")
}
return joinDispatcherParams("alterzorder", mode, luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.window.set_prop("):
prop := luaTableStringField(expr, "prop")
if prop == "" {
prop = luaTableStringField(expr, "property")
}
return joinDispatcherParams("setprop", luaTableStringField(expr, "window"), prop, luaTableStringField(expr, "value"))
case strings.HasPrefix(expr, "hl.dsp.workspace.rename("):
return joinDispatcherParams("renameworkspace", luaTableStringField(expr, "workspace"), luaTableStringField(expr, "name"))
case strings.HasPrefix(expr, "hl.dsp.workspace.move("):
workspace := luaTableStringField(expr, "workspace")
monitor := luaTableStringField(expr, "monitor")
if workspace != "" {
return joinDispatcherParams("moveworkspacetomonitor", workspace, monitor)
}
return "movecurrentworkspacetomonitor", monitor
case strings.HasPrefix(expr, "hl.dsp.workspace.swap_monitors("):
return joinDispatcherParams("swapactiveworkspaces", luaTableStringField(expr, "monitor1"), luaTableStringField(expr, "monitor2"))
case strings.HasPrefix(expr, "hl.dsp.workspace.toggle_special("):
return "togglespecialworkspace", luaCallStringArgValue(expr, "hl.dsp.workspace.toggle_special")
case strings.HasPrefix(expr, "hl.dsp.layout("):
if arg := luaCallStringArgValue(expr, "hl.dsp.layout"); arg != "" {
return "layoutmsg", arg
arg := extractLuaCallStringArg(expr, "hl.dsp.layout")
if arg != "" {
if u, err := strconv.Unquote(arg); err == nil {
return "layoutmsg", u
}
}
case strings.HasPrefix(expr, "hl.dsp.dpms("):
if action := luaTableStringField(expr, "action"); action != "" {
switch action {
case "enable":
return "dpms", "on"
case "disable":
return "dpms", "off"
}
return "dpms", action
}
return "dpms", ""
case strings.HasPrefix(expr, "hl.dsp.submap("):
return "submap", luaCallStringArgValue(expr, "hl.dsp.submap")
case strings.HasPrefix(expr, "hl.dsp.global("):
return "global", luaCallStringArgValue(expr, "hl.dsp.global")
case strings.HasPrefix(expr, "hl.dsp.event("):
return "event", luaCallStringArgValue(expr, "hl.dsp.event")
case strings.HasPrefix(expr, "hl.dsp.pass("):
if window := luaTableStringField(expr, "window"); window != "" {
return "pass", window
}
return "pass", luaCallStringArgValue(expr, "hl.dsp.pass")
case strings.HasPrefix(expr, "hl.dsp.send_shortcut("):
return joinDispatcherParams("sendshortcut", luaTableModsField(expr), luaTableStringField(expr, "key"), luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.send_key_state("):
return joinDispatcherParams("sendkeystate", luaTableModsField(expr), luaTableStringField(expr, "key"), luaTableStringField(expr, "state"), luaTableStringField(expr, "window"))
case strings.HasPrefix(expr, "hl.dsp.cursor.move_to_corner("):
return "movecursortocorner", luaStringValue(luaTableScalarField(expr, "corner"))
case strings.HasPrefix(expr, "hl.dsp.cursor.move("):
return joinDispatcherParams("movecursor", luaStringValue(luaTableScalarField(expr, "x")), luaStringValue(luaTableScalarField(expr, "y")))
case strings.Contains(expr, "hl.dsp.force_renderer_reload()"):
return "forcerendererreload", ""
case strings.HasPrefix(expr, "hl.dsp.force_idle("):
return "forceidle", luaCallScalarArgValue(expr, "hl.dsp.force_idle")
case strings.Contains(expr, "hl.dsp.exit()"):
return "exit", ""
default:
return expr, ""
}
return expr, ""
}
func splitDispatchCommand(command string) (dispatcher, params string) {
command = strings.TrimSpace(command)
if command == "" {
return "", ""
}
parts := strings.SplitN(command, " ", 2)
if len(parts) == 1 {
return parts[0], ""
}
return parts[0], strings.TrimSpace(parts[1])
}
func joinDispatcherParams(dispatcher string, values ...string) (string, string) {
parts := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
parts = append(parts, value)
}
}
return dispatcher, strings.Join(parts, " ")
}
func luaEmbeddedCallStringArgValue(expr, funcName string) string {
idx := strings.Index(expr, funcName+"(")
if idx < 0 {
return ""
}
return luaCallStringArgValue(expr[idx:], funcName)
}
func luaCallScalarArgValue(callExpr, funcName string) string {
callExpr = strings.TrimSpace(callExpr)
prefix := funcName + "("
if !strings.HasPrefix(callExpr, prefix) {
return ""
}
inner := strings.TrimSpace(callExpr[len(prefix):])
if inner == "" {
return ""
}
if s := luaCallStringArgValue(callExpr, funcName); s != "" {
return s
}
re := regexp.MustCompile(`^-?\d+(?:\.\d+)?`)
return re.FindString(inner)
}
func luaToggleActionToLegacy(action string) string {
switch strings.ToLower(strings.TrimSpace(action)) {
case "on", "enable", "enabled", "set", "lock":
return "on"
case "off", "disable", "disabled", "unset", "unlock":
return "off"
default:
return "toggle"
}
}
func luaToggleActionToLockArg(action string) string {
switch luaToggleActionToLegacy(action) {
case "on":
return "lock"
case "off":
return "unlock"
default:
return "toggle"
return "exec", "hyprctl dispatch lua:" + expr
}
return "exec", "hyprctl dispatch lua:" + expr
}
func extractLuaCallStringArg(callExpr, funcName string) string {
@@ -1421,46 +1100,10 @@ func extractLuaCallStringArg(callExpr, funcName string) string {
return ""
}
func luaCallStringArgValue(callExpr, funcName string) string {
arg := extractLuaCallStringArg(callExpr, funcName)
if arg == "" {
return ""
}
u, err := strconv.Unquote(arg)
if err != nil {
return ""
}
return u
}
func luaTableStringField(expr, field string) string {
return luaStringValue(luaTableScalarField(expr, field))
}
func luaTableModsField(expr string) string {
if mods := luaTableStringField(expr, "mods"); mods != "" {
return mods
}
return luaTableStringField(expr, "mod")
}
func luaTableBoolFieldValue(expr, field string) bool {
value, ok := luaTableBoolField(expr, field)
return ok && value
}
func luaTableBoolField(expr, field string) (bool, bool) {
raw := strings.ToLower(luaTableScalarField(expr, field))
switch raw {
case "true":
return true, true
case "false":
return false, true
default:
return false, false
}
}
func luaTableScalarField(expr, field string) string {
re := regexp.MustCompile(`(?s)\b` + regexp.QuoteMeta(field) + `\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\[\[.*?\]\]|-?\d+(?:\.\d+)?|true|false)`)
m := re.FindStringSubmatch(expr)
@@ -1493,38 +1136,8 @@ func luaStringValue(raw string) string {
}
func luaLineTrailingComment(line string) string {
inString := byte(0)
escaped := false
for i := 0; i < len(line)-1; i++ {
c := line[i]
if inString != 0 {
if escaped {
escaped = false
continue
}
if c == '\\' && inString == '"' {
escaped = true
continue
}
if c == inString {
inString = 0
}
continue
}
if c == '"' || c == '\'' {
inString = c
continue
}
if c == '[' && line[i+1] == '[' {
if end := strings.Index(line[i+2:], "]]"); end >= 0 {
i += end + 3
continue
}
return ""
}
if c == '-' && line[i+1] == '-' {
return strings.TrimSpace(line[i+2:])
}
if idx := strings.Index(line, "--"); idx >= 0 {
return strings.TrimSpace(line[idx+2:])
}
return ""
}
@@ -70,37 +70,12 @@ func TestHyprlandLuaBindRoundTripHelpers(t *testing.T) {
wantParams string
}{
{`hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]])`, "exec", `dms ipc call brightness increment 5 ""`},
{`hl.dsp.exec_cmd([[hyprctl dispatch workspace 1]])`, "workspace", "1"},
{`hl.dispatch("workspace 2")`, "workspace", "2"},
{`hl.dispatch([[customdispatcher arg one]])`, "customdispatcher", "arg one"},
{`hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, "fullscreen", "1"},
{`hl.dsp.window.float({ action = "on" })`, "setfloating", ""},
{`hl.dsp.window.close()`, "killactive", ""},
{`hl.dsp.window.kill()`, "forcekillactive", ""},
{`hl.dsp.window.close({ window = "class:^(kitty)$" })`, "closewindow", "class:^(kitty)$"},
{`hl.dsp.focus({ workspace = "e+1" })`, "workspace", "e+1"},
{`hl.dsp.focus({ workspace = "2", on_current_monitor = true })`, "focusworkspaceoncurrentmonitor", "2"},
{`hl.dsp.window.move({ monitor = "l" })`, "movewindow", "mon:l"},
{`hl.dsp.window.move({ direction = "r", group_aware = true })`, "movewindoworgroup", "r"},
{`hl.dsp.window.move({ into_group = "l" })`, "moveintogroup", "l"},
{`hl.dsp.window.move({ out_of_group = true })`, "moveoutofgroup", ""},
{`hl.dsp.window.move({ workspace = "special:magic", follow = false })`, "movetoworkspacesilent", "special:magic"},
{`hl.dsp.window.resize({ x = -100, y = 0, relative = true })`, "resizeactive", "-100 0"},
{`hl.dsp.window.resize({ x = 1280, y = 720, relative = false })`, "resizeactive", "exact 1280 720"},
{`hl.dsp.window.resize({ x = 100, y = 50, relative = true, window = "class:^(app)$" })`, "resizewindowpixel", "100 50,class:^(app)$"},
{`hl.dsp.window.cycle_next({ next = false, tiled = true })`, "cyclenext", "prev tiled"},
{`hl.dsp.group.next()`, "changegroupactive", "f"},
{`hl.dsp.group.prev()`, "changegroupactive", "b"},
{`hl.dsp.group.active({ index = 2 })`, "changegroupactive", "2"},
{`hl.dsp.group.move_window({ forward = false })`, "movegroupwindow", "b"},
{`hl.dsp.group.lock({ action = "on" })`, "lockgroups", "lock"},
{`hl.dsp.group.lock_active({ action = "off" })`, "lockactivegroup", "unlock"},
{`hl.dsp.window.deny_from_group({ action = "toggle" })`, "denywindowfromgroup", "toggle"},
{`function() hl.exec_cmd("hyprctl dispatch splitratio +0.1") end`, "splitratio", "+0.1"},
{`hl.dsp.window.resize({ x = "-10%", y = 0, relative = true })`, "resizeactive", "-10% 0"},
{`hl.dsp.layout("togglesplit")`, "layoutmsg", "togglesplit"},
{`hl.dsp.dpms({ action = "toggle" })`, "dpms", "toggle"},
{`hl.dsp.workspace.rename({ workspace = "1", name = "work" })`, "renameworkspace", "1 work"},
{`hl.dsp.no_op()`, "hl.dsp.no_op()", ""},
}
for _, tt := range tests {
@@ -138,132 +113,12 @@ func TestWriteLuaBindLineMapsSpawnActionForHyprland(t *testing.T) {
})
want := `hl.unbind("SUPER + N")
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"), { description = "Notepad: Toggle" })`
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) -- Notepad: Toggle`
if got := strings.TrimSpace(sb.String()); got != want {
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
}
}
func TestWriteLuaBindLineLeavesCustomLuaDispatcherRaw(t *testing.T) {
var sb strings.Builder
writeLuaBindLine(&sb, &hyprlandOverrideBind{
Key: "Super+u",
Action: "hl.dsp.no_op()",
Description: "Custom Lua",
})
want := `hl.unbind("SUPER + U")
hl.bind("SUPER + U", hl.dsp.no_op(), { description = "Custom Lua" })`
if got := strings.TrimSpace(sb.String()); got != want {
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
}
}
func TestLuaActionStringFromHyprlangActionUsesNativeDispatchers(t *testing.T) {
tests := []struct {
action string
want string
}{
{"killactive", `hl.dsp.window.close()`},
{"forcekillactive", `hl.dsp.window.kill()`},
{"workspace 1", `hl.dsp.focus({ workspace = "1" })`},
{"movetoworkspace 2", `hl.dsp.window.move({ workspace = "2" })`},
{"movetoworkspacesilent special:magic", `hl.dsp.window.move({ workspace = "special:magic", follow = false })`},
{"focusmonitor DP-1", `hl.dsp.focus({ monitor = "DP-1" })`},
{"resizeactive exact 1280 720", `hl.dsp.window.resize({ x = 1280, y = 720, relative = false })`},
{"dpms toggle", `hl.dsp.dpms({ action = "toggle" })`},
{"renameworkspace 1 work", `hl.dsp.workspace.rename({ workspace = "1", name = "work" })`},
{"changegroupactive f", `hl.dsp.group.next()`},
{"changegroupactive b", `hl.dsp.group.prev()`},
{"changegroupactive 2", `hl.dsp.group.active({ index = 2 })`},
{"moveintogroup l", `hl.dsp.window.move({ into_group = "l" })`},
{"moveoutofgroup", `hl.dsp.window.move({ out_of_group = true })`},
{"movewindoworgroup r", `hl.dsp.window.move({ direction = "r", group_aware = true })`},
{"movegroupwindow b", `hl.dsp.group.move_window({ forward = false })`},
{"lockgroups lock", `hl.dsp.group.lock({ action = "on" })`},
{"lockactivegroup unlock", `hl.dsp.group.lock_active({ action = "off" })`},
{"denywindowfromgroup toggle", `hl.dsp.window.deny_from_group({ action = "toggle" })`},
{"cyclenext prev", `hl.dsp.window.cycle_next({ next = false })`},
{"setfloating", `hl.dsp.window.float({ action = "on" })`},
{"settiled", `hl.dsp.window.float({ action = "off" })`},
{"bringactivetotop", `hl.dsp.window.bring_to_top()`},
{"toggleswallow", `hl.dsp.window.toggle_swallow()`},
{"forceidle 300", `hl.dsp.force_idle(300)`},
}
for _, tt := range tests {
t.Run(tt.action, func(t *testing.T) {
got := luaActionStringFromHyprlangAction(tt.action)
if got != tt.want {
t.Fatalf("luaActionStringFromHyprlangAction(%q) = %q, want %q", tt.action, got, tt.want)
}
if strings.Contains(got, "hyprctl dispatch") {
t.Fatalf("expected native Lua dispatcher, got legacy dispatch wrapper: %q", got)
}
})
}
}
func TestLuaActionStringFallsBackForUnsupportedResizePercentages(t *testing.T) {
got := luaActionStringFromHyprlangAction("resizeactive exact 100% 100%")
want := `function() hl.exec_cmd("hyprctl dispatch resizeactive exact 100% 100%") end`
if got != want {
t.Fatalf("luaActionStringFromHyprlangAction() = %q, want %q", got, want)
}
}
func TestParseLuaBindLineHandlesFunctionDispatcherFallback(t *testing.T) {
line := `hl.bind("SUPER + R", function() hl.exec_cmd("hyprctl dispatch resizeactive exact 100% 100%") end, { description = "Unsupported Resize" })`
got, ok := parseLuaBindOverrideLine(line)
if !ok {
t.Fatalf("expected line to parse")
}
if got.Action != "resizeactive exact 100% 100%" {
t.Fatalf("Action = %q, want resizeactive exact 100%% 100%%", got.Action)
}
if got.Description != "Unsupported Resize" {
t.Fatalf("Description = %q, want Unsupported Resize", got.Description)
}
}
func TestLuaActionStringLeavesCustomLuaDispatcherRaw(t *testing.T) {
got := luaActionStringFromHyprlangAction("hl.dsp.no_op()")
want := `hl.dsp.no_op()`
if got != want {
t.Fatalf("luaActionStringFromHyprlangAction() = %q, want %q", got, want)
}
if strings.Contains(got, "hl.dispatch") || strings.Contains(got, "hyprctl dispatch") {
t.Fatalf("expected custom Lua dispatcher expression to stay raw, got %q", got)
}
}
func TestReadLuaOverrideMigratesTrailingCommentToDescription(t *testing.T) {
tmpDir := t.TempDir()
overridePath := filepath.Join(tmpDir, "binds-user.lua")
contents := `hl.unbind("SUPER + N")
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle")) -- Notepad: Toggle
hl.bind("SUPER + H", hl.dsp.exec_cmd("app --help"))
`
if err := os.WriteFile(overridePath, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
binds, err := readLuaOrHyprlangOverride(overridePath)
if err != nil {
t.Fatal(err)
}
got := binds["super+n"]
if got == nil {
t.Fatalf("expected SUPER+N override, got %#v", binds)
}
if got.Description != "Notepad: Toggle" {
t.Fatalf("expected trailing comment to be preserved as description, got %q", got.Description)
}
if got := binds["super+h"]; got == nil || got.Description != "" {
t.Fatalf("expected -- inside a Lua string to stay out of the description, got %#v", got)
}
}
func TestHyprlandLuaBindsUserOverridesDefaults(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
@@ -428,64 +283,6 @@ func TestHyprlandRemoveBindWritesNegativeOverrideForDefault(t *testing.T) {
}
}
func TestHyprlandSetBindLeavesConfOnlyInstallReadOnly(t *testing.T) {
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte("bind = SUPER, T, exec, kitty\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
err := provider.SetBind("SUPER+N", "workspace 1", "Workspace 1", nil)
if err == nil {
t.Fatal("expected SetBind to reject conf-only Hyprland config")
}
if !strings.Contains(err.Error(), "read-only") {
t.Fatalf("expected read-only error, got %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "dms", "binds-user.lua")); !os.IsNotExist(err) {
t.Fatalf("expected no Lua override to be created for conf-only config, stat err=%v", err)
}
}
func TestHyprlandSetBindUpdatesSpacedLuaOverrideWithoutDuplicates(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatal(err)
}
override := `-- DMS user keybind overrides
hl.unbind("SUPER + SHIFT + S")
hl.bind("SUPER + 1", hl.dsp.exec_cmd("hyprctl dispatch workspace 1"))
`
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandProvider(tmpDir)
if err := provider.SetBind("SUPER + 1", "workspace 1", "", nil); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
if err != nil {
t.Fatal(err)
}
got := string(data)
if strings.Count(got, `hl.unbind("SUPER + 1")`) != 1 {
t.Fatalf("expected one SUPER+1 unbind, got:\n%s", got)
}
if strings.Count(got, `hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))`) != 1 {
t.Fatalf("expected one native SUPER+1 bind, got:\n%s", got)
}
if strings.Contains(got, "hyprctl dispatch workspace 1") {
t.Fatalf("expected old hyprctl workspace dispatcher to be replaced, got:\n%s", got)
}
if !strings.Contains(got, `hl.unbind("SUPER + SHIFT + S")`) {
t.Fatalf("expected unrelated override to be preserved, got:\n%s", got)
}
}
func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
-2
View File
@@ -25,8 +25,6 @@ type DMSBindsStatus struct {
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
}
type CheatSheet struct {
@@ -2,7 +2,6 @@ package clipboard
import (
"encoding/json"
"errors"
"fmt"
"net"
@@ -74,10 +73,6 @@ func handleGetEntry(conn net.Conn, req models.Request, m *Manager) {
entry, err := m.GetEntry(uint64(id))
if err != nil {
if errors.Is(err, errEntryNotFound) {
models.Respond[any](conn, req.ID, nil)
return
}
models.RespondError(conn, req.ID, err.Error())
return
}
+6 -25
View File
@@ -3,7 +3,6 @@ package clipboard
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"image"
_ "image/gif"
@@ -35,8 +34,6 @@ import (
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
var errEntryNotFound = errors.New("entry not found")
// These mime types won't be stored in history
var sensitiveMimeTypes = []string{
"x-kde-passwordManagerHint",
@@ -575,16 +572,16 @@ func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
func (m *Manager) selectMimeType(mimes []string) string {
preferredTypes := []string{
"text/uri-list",
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"image/tiff",
"text/plain;charset=utf-8",
"text/plain",
"UTF8_STRING",
"STRING",
"TEXT",
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"image/tiff",
}
for _, pref := range preferredTypes {
@@ -767,25 +764,9 @@ func stateEqual(a, b *State) bool {
if len(a.History) != len(b.History) {
return false
}
for i := range a.History {
if !entryStateEqual(a.History[i], b.History[i]) {
return false
}
}
return true
}
func entryStateEqual(a, b Entry) bool {
return a.ID == b.ID &&
a.Hash == b.Hash &&
a.Pinned == b.Pinned &&
a.IsImage == b.IsImage &&
a.MimeType == b.MimeType &&
a.Preview == b.Preview &&
a.Size == b.Size &&
a.Timestamp.Equal(b.Timestamp)
}
func (m *Manager) GetHistory() []Entry {
if m.db == nil {
return nil
@@ -873,7 +854,7 @@ func (m *Manager) GetEntry(id uint64) (*Entry, error) {
return nil, err
}
if !found {
return nil, errEntryNotFound
return nil, fmt.Errorf("entry not found")
}
return &entry, nil
+2 -141
View File
@@ -1,52 +1,17 @@
package clipboard
import (
"bytes"
"encoding/json"
"net"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
)
type clipboardTestConn struct {
net.Conn
writeBuf *bytes.Buffer
}
func newClipboardTestConn() *clipboardTestConn {
return &clipboardTestConn{writeBuf: &bytes.Buffer{}}
}
func (c *clipboardTestConn) Write(b []byte) (int, error) {
return c.writeBuf.Write(b)
}
func newTestManagerWithDB(t *testing.T) *Manager {
t.Helper()
db, err := openDB(filepath.Join(t.TempDir(), "clipboard.db"))
require.NoError(t, err)
t.Cleanup(func() {
db.Close()
})
return &Manager{
config: DefaultConfig(),
db: db,
}
}
func TestEncodeDecodeEntry_Roundtrip(t *testing.T) {
original := Entry{
ID: 12345,
@@ -166,113 +131,11 @@ func TestStateEqual_HistoryLengthDiffers(t *testing.T) {
}
func TestStateEqual_BothEqual(t *testing.T) {
ts := time.Now().Truncate(time.Second)
entry := Entry{
ID: 1,
Hash: 100,
MimeType: "image/png",
Preview: "[[ image 1 KiB png 32x32 ]]",
Size: 1024,
Timestamp: ts,
IsImage: true,
Pinned: true,
}
a := &State{Enabled: true, History: []Entry{entry}}
b := &State{Enabled: true, History: []Entry{entry}}
a := &State{Enabled: true, History: []Entry{{ID: 1}, {ID: 2}}}
b := &State{Enabled: true, History: []Entry{{ID: 3}, {ID: 4}}}
assert.True(t, stateEqual(a, b))
}
func TestStateEqual_SameLengthDifferentIDs(t *testing.T) {
ts := time.Now().Truncate(time.Second)
a := &State{Enabled: true, History: []Entry{{ID: 1, Hash: 100, Timestamp: ts}}}
b := &State{Enabled: true, History: []Entry{{ID: 2, Hash: 100, Timestamp: ts}}}
assert.False(t, stateEqual(a, b))
}
func TestStateEqual_MetadataDiffers(t *testing.T) {
ts := time.Now().Truncate(time.Second)
base := Entry{
ID: 1,
Hash: 100,
MimeType: "image/png",
Preview: "[[ image 1 KiB png 32x32 ]]",
Size: 1024,
Timestamp: ts,
IsImage: true,
Pinned: false,
}
tests := []struct {
name string
mutate func(*Entry)
}{
{name: "hash", mutate: func(e *Entry) { e.Hash = 101 }},
{name: "pinned", mutate: func(e *Entry) { e.Pinned = true }},
{name: "is image", mutate: func(e *Entry) { e.IsImage = false }},
{name: "mime type", mutate: func(e *Entry) { e.MimeType = "image/jpeg" }},
{name: "preview", mutate: func(e *Entry) { e.Preview = "[[ image 2 KiB jpeg 64x64 ]]" }},
{name: "size", mutate: func(e *Entry) { e.Size = 2048 }},
{name: "timestamp", mutate: func(e *Entry) { e.Timestamp = ts.Add(time.Second) }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changed := base
tt.mutate(&changed)
a := &State{Enabled: true, History: []Entry{base}}
b := &State{Enabled: true, History: []Entry{changed}}
assert.False(t, stateEqual(a, b))
})
}
}
func TestHandleGetEntry_ReturnsExistingEntry(t *testing.T) {
m := newTestManagerWithDB(t)
err := m.storeEntry(Entry{
Data: []byte("hello world"),
MimeType: "text/plain;charset=utf-8",
Preview: "hello world",
Size: len("hello world"),
Timestamp: time.Now().Truncate(time.Second),
IsImage: false,
})
require.NoError(t, err)
history := m.GetHistory()
require.Len(t, history, 1)
conn := newClipboardTestConn()
handleGetEntry(conn, models.Request{
ID: 1,
Params: map[string]any{"id": float64(history[0].ID)},
}, m)
var resp models.Response[Entry]
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
require.NotNil(t, resp.Result)
assert.Equal(t, history[0].ID, resp.Result.ID)
assert.Equal(t, []byte("hello world"), resp.Result.Data)
}
func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
m := newTestManagerWithDB(t)
conn := newClipboardTestConn()
handleGetEntry(conn, models.Request{
ID: 1,
Params: map[string]any{"id": float64(999)},
}, m)
var resp models.Response[any]
require.NoError(t, json.NewDecoder(conn.writeBuf).Decode(&resp))
assert.Empty(t, resp.Error)
assert.Nil(t, resp.Result)
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{
subscribers: make(map[string]chan State),
@@ -547,8 +410,6 @@ func TestSelectMimeType(t *testing.T) {
{[]string{"text/plain;charset=utf-8", "text/html"}, "text/plain;charset=utf-8"},
{[]string{"text/html", "text/plain"}, "text/plain"},
{[]string{"text/html", "image/png"}, "image/png"},
{[]string{"image/png", "text/plain"}, "image/png"},
{[]string{"text/plain", "image/png"}, "image/png"},
{[]string{"image/png", "image/jpeg"}, "image/png"},
{[]string{"image/png"}, "image/png"},
{[]string{"application/octet-stream"}, "application/octet-stream"},
@@ -27,19 +27,16 @@ type linkInfo struct {
}
func (l *linkInfo) isWired() bool {
if looksVirtual(l.name) {
return false
}
if l.linkType != "" {
return l.linkType == "ether"
}
return !strings.HasPrefix(l.name, "wlan") && !strings.HasPrefix(l.name, "wlp")
if looksVirtual(l.name) || strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp") {
return false
}
return true
}
func (l *linkInfo) isWireless() bool {
if looksVirtual(l.name) {
return false
}
if l.linkType != "" {
return l.linkType == "wlan"
}
@@ -48,7 +45,7 @@ func (l *linkInfo) isWireless() bool {
func looksVirtual(name string) bool {
virtualPrefixes := []string{
"lo", "docker", "podman", "veth", "virbr", "br-", "vnet", "tun", "tap",
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
}
for _, prefix := range virtualPrefixes {
@@ -113,12 +110,6 @@ func (b *SystemdNetworkdBackend) Close() {
}
}
type enumeratedLink struct {
ifindex int32
name string
path dbus.ObjectPath
}
func (b *SystemdNetworkdBackend) enumerateLinks() error {
obj := b.conn.Object(networkdBusName, b.managerPath)
@@ -132,48 +123,25 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
return fmt.Errorf("ListLinks: %w", err)
}
fresh := make([]enumeratedLink, len(links))
for i, l := range links {
fresh[i] = enumeratedLink{ifindex: l.Ifindex, name: l.Name, path: l.Path}
}
b.linksMutex.Lock()
defer b.linksMutex.Unlock()
b.syncLinks(fresh)
return nil
}
// syncLinks reconciles the cached link map against the freshly enumerated set:
// it adds links not seen before (querying their Type once), refreshes the
// ifindex of survivors, and prunes links that no longer appear. Pruning is what
// keeps torn-down container interfaces (podman bridges, veth pairs) from
// lingering as routable and being mistaken for the wired uplink.
// Callers must hold linksMutex.
func (b *SystemdNetworkdBackend) syncLinks(fresh []enumeratedLink) {
present := make(map[string]bool, len(fresh))
for _, l := range fresh {
present[l.name] = true
if existing, ok := b.links[l.name]; ok && existing.path == l.path {
existing.ifindex = l.ifindex
for _, l := range links {
if existing, ok := b.links[l.Name]; ok && existing.path == l.Path {
existing.ifindex = l.Ifindex
continue
}
info := &linkInfo{
ifindex: l.ifindex,
name: l.name,
path: l.path,
linkType: b.fetchLinkType(l.path),
ifindex: l.Ifindex,
name: l.Name,
path: l.Path,
linkType: b.fetchLinkType(l.Path),
}
b.links[l.name] = info
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s, type=%q)", l.name, l.ifindex, l.path, info.linkType)
b.links[l.Name] = info
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s, type=%q)", l.Name, l.Ifindex, l.Path, info.linkType)
}
for name := range b.links {
if !present[name] {
log.Debugf("networkd: pruned stale link %s", name)
delete(b.links, name)
}
}
return nil
}
// fetchLinkType queries networkd's Describe method and extracts the link Type
@@ -160,12 +160,6 @@ func TestLinkInfo_Classify(t *testing.T) {
{"loopback type", "lo", "loopback", false, false},
{"none type (tun overlay)", "nebula.homelab", "none", false, false},
{"none type (wireguard)", "wg0", "none", false, false},
// Virtual interfaces report Type=ether but must never be mistaken for
// the wired uplink — stale podman/veth links would otherwise poison
// ethernet detection.
{"veth ether excluded", "veth1234", "ether", false, false},
{"podman bridge ether excluded", "podman3", "ether", false, false},
{"docker bridge ether excluded", "docker0", "ether", false, false},
// Fallback path: linkType unavailable, name-prefix heuristic applies.
{"fallback enp wired", "enp141s0", "", true, false},
{"fallback wlan wireless", "wlan0", "", false, true},
@@ -211,46 +205,8 @@ func TestParseDescribeType(t *testing.T) {
}
}
func TestSyncLinks_PrunesRemovedLinks(t *testing.T) {
// Stale container interfaces (torn-down podman bridges, veth pairs) must
// not linger in the link map after they disappear from ListLinks — kept as
// routable, they stole the wired-uplink slot from the real ethernet NIC.
backend, _ := NewSystemdNetworkdBackend()
backend.links = map[string]*linkInfo{
"eno1": {ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32", linkType: "ether", opState: "routable"},
"podman3": {ifindex: 9, name: "podman3", path: "/org/freedesktop/network1/link/_39", linkType: "ether", opState: "routable"},
"veth0": {ifindex: 10, name: "veth0", path: "/org/freedesktop/network1/link/_310", linkType: "ether", opState: "routable"},
}
backend.syncLinks([]enumeratedLink{
{ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32"},
})
assert.Len(t, backend.links, 1)
assert.Contains(t, backend.links, "eno1")
assert.NotContains(t, backend.links, "podman3")
assert.NotContains(t, backend.links, "veth0")
}
func TestSyncLinks_RefreshesSurvivingLink(t *testing.T) {
// A link that survives keeps its cached Type — Describe is only queried for
// newly seen links — while picking up a refreshed ifindex.
backend, _ := NewSystemdNetworkdBackend()
backend.links = map[string]*linkInfo{
"eno1": {ifindex: 2, name: "eno1", path: "/org/freedesktop/network1/link/_32", linkType: "ether"},
}
backend.syncLinks([]enumeratedLink{
{ifindex: 7, name: "eno1", path: "/org/freedesktop/network1/link/_32"},
})
assert.Len(t, backend.links, 1)
assert.Equal(t, int32(7), backend.links["eno1"].ifindex)
assert.Equal(t, "ether", backend.links["eno1"].linkType)
}
func TestLooksVirtual(t *testing.T) {
virtual := []string{"lo", "docker0", "veth123", "virbr0", "br-abc", "vnet0", "tun0", "tap0", "vboxnet0", "vmnet1", "kube-ipvs0", "cni0", "flannel.1", "cali-abc", "podman0", "podman3"}
virtual := []string{"lo", "docker0", "veth123", "virbr0", "br-abc", "vnet0", "tun0", "tap0", "vboxnet0", "vmnet1", "kube-ipvs0", "cni0", "flannel.1", "cali-abc"}
for _, n := range virtual {
assert.True(t, looksVirtual(n), "%s should look virtual", n)
}
-1
View File
@@ -418,7 +418,6 @@ func handleConnection(conn net.Conn) {
conn.Write(capsData)
conn.Write([]byte("\n"))
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), 64*1024*1024) // grow up to 64 MB for large clipboard payloads
for scanner.Scan() {
line := scanner.Bytes()
@@ -44,8 +44,6 @@ type HyprlandRulesParser struct {
dmsIncludePos int
rulesAfterDMS int
dmsProcessed bool
configFormat string
readOnly bool
requireLineInMain int // hyprland.lua line (1-based) where require("dms.windowrules") occurs; else -1
primaryHyprLua string // absolute path to ~/.config/hypr/hyprland.lua when that is the main config
@@ -84,15 +82,10 @@ func (p *HyprlandRulesParser) Parse() ([]HyprlandWindowRule, error) {
}
if strings.EqualFold(filepath.Ext(mainConfig), ".lua") {
p.configFormat = "lua"
p.readOnly = false
p.probeRequireWindowrulesLine(mainConfig)
if ap, err := filepath.Abs(mainConfig); err == nil {
p.primaryHyprLua = ap
}
} else {
p.configFormat = "hyprlang"
p.readOnly = true
}
if err := p.parseFile(mainConfig); err != nil {
@@ -307,8 +300,6 @@ func (p *HyprlandRulesParser) buildDMSStatus() *windowrules.DMSRulesStatus {
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
RulesAfterDMS: p.rulesAfterDMS,
ConfigFormat: p.configFormat,
ReadOnly: p.readOnly,
}
switch {
@@ -460,9 +451,6 @@ func (p *HyprlandWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
}
func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules()
if err != nil {
rules = []windowrules.WindowRule{}
@@ -484,9 +472,6 @@ func (p *HyprlandWritableProvider) SetRule(rule windowrules.WindowRule) error {
}
func (p *HyprlandWritableProvider) RemoveRule(id string) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules()
if err != nil {
return err
@@ -503,9 +488,6 @@ func (p *HyprlandWritableProvider) RemoveRule(id string) error {
}
func (p *HyprlandWritableProvider) ReorderRules(ids []string) error {
if err := p.ensureWritableConfig(); err != nil {
return err
}
rules, err := p.LoadDMSRules()
if err != nil {
return err
@@ -531,29 +513,6 @@ func (p *HyprlandWritableProvider) ReorderRules(ids []string) error {
return p.writeDMSRules(newRules)
}
func (p *HyprlandWritableProvider) ensureWritableConfig() error {
if p.isLegacyConfigReadOnly() {
return fmt.Errorf("hyprland legacy conf configs are read-only; run dms setup to migrate to Lua before editing window rules")
}
return nil
}
func (p *HyprlandWritableProvider) isLegacyConfigReadOnly() bool {
expanded, err := utils.ExpandPath(p.configDir)
if err != nil {
expanded = p.configDir
}
luaPath := filepath.Join(expanded, "hyprland.lua")
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
return false
}
confPath := filepath.Join(expanded, "hyprland.conf")
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
return true
}
return false
}
var dmsRuleCommentRegex = regexp.MustCompile(`^#\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`)
var dmsRuleLuaHDRRegex = regexp.MustCompile(`^\s*--\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`)
@@ -188,27 +188,6 @@ func TestHyprlandSetAndLoadDMSRules(t *testing.T) {
}
}
func TestHyprlandSetRuleLeavesConfOnlyInstallReadOnly(t *testing.T) {
tmpDir := t.TempDir()
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte("windowrulev2 = float, class:^(kitty)$\n"), 0o644); err != nil {
t.Fatal(err)
}
provider := NewHyprlandWritableProvider(tmpDir)
rule := newTestWindowRule("test_id", "Test Rule", "^(firefox)$")
rule.Actions.OpenFloating = boolPtr(true)
err := provider.SetRule(rule)
if err == nil {
t.Fatal("expected SetRule to reject conf-only Hyprland config")
}
if !strings.Contains(err.Error(), "read-only") {
t.Fatalf("expected read-only error, got %v", err)
}
if _, err := os.Stat(filepath.Join(tmpDir, "dms", "windowrules.lua")); !os.IsNotExist(err) {
t.Fatalf("expected no Lua windowrules file to be created for conf-only config, stat err=%v", err)
}
}
func TestHyprlandRemoveRule(t *testing.T) {
tmpDir := t.TempDir()
provider := NewHyprlandWritableProvider(tmpDir)
@@ -14,18 +14,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
)
type NiriMatch struct {
AppID string
Title string
IsFloating *bool
IsActive *bool
IsFocused *bool
IsActiveInColumn *bool
IsWindowCastTarget *bool
IsUrgent *bool
AtStartup *bool
}
type NiriWindowRule struct {
MatchAppID string
MatchTitle string
@@ -36,7 +24,6 @@ type NiriWindowRule struct {
MatchIsWindowCastTarget *bool
MatchIsUrgent *bool
MatchAtStartup *bool
Matches []NiriMatch
Opacity *float64
OpenFloating *bool
OpenMaximized *bool
@@ -63,10 +50,6 @@ type NiriWindowRule struct {
FocusRingOff *bool
BorderOff *bool
DrawBorderWithBg *bool
BgBlur *bool
BgXray *bool
BgNoise *float64
BgSaturation *float64
Source string
}
@@ -208,7 +191,7 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
switch childName {
case "match":
rule.Matches = append(rule.Matches, p.parseMatchNode(child))
p.parseMatchNode(child, &rule)
case "opacity":
if len(child.Arguments) > 0 {
val := child.Arguments[0].ResolvedValue()
@@ -314,24 +297,9 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
case "draw-border-with-background":
b := p.parseBoolArg(child)
rule.DrawBorderWithBg = &b
case "background-effect":
p.parseBackgroundEffectNode(child, &rule)
}
}
if len(rule.Matches) > 0 {
first := rule.Matches[0]
rule.MatchAppID = first.AppID
rule.MatchTitle = first.Title
rule.MatchIsFloating = first.IsFloating
rule.MatchIsActive = first.IsActive
rule.MatchIsFocused = first.IsFocused
rule.MatchIsActiveInColumn = first.IsActiveInColumn
rule.MatchIsWindowCastTarget = first.IsWindowCastTarget
rule.MatchIsUrgent = first.IsUrgent
rule.MatchAtStartup = first.AtStartup
}
p.rules = append(p.rules, rule)
}
@@ -358,47 +326,45 @@ func (p *NiriRulesParser) parseSizeNode(node *document.Node) string {
return ""
}
func (p *NiriRulesParser) parseMatchNode(node *document.Node) NiriMatch {
m := NiriMatch{}
func (p *NiriRulesParser) parseMatchNode(node *document.Node, rule *NiriWindowRule) {
if node.Properties == nil {
return m
return
}
if val, ok := node.Properties.Get("app-id"); ok {
m.AppID = val.ValueString()
rule.MatchAppID = val.ValueString()
}
if val, ok := node.Properties.Get("title"); ok {
m.Title = val.ValueString()
rule.MatchTitle = val.ValueString()
}
if val, ok := node.Properties.Get("is-floating"); ok {
b := val.ValueString() == "true"
m.IsFloating = &b
rule.MatchIsFloating = &b
}
if val, ok := node.Properties.Get("is-active"); ok {
b := val.ValueString() == "true"
m.IsActive = &b
rule.MatchIsActive = &b
}
if val, ok := node.Properties.Get("is-focused"); ok {
b := val.ValueString() == "true"
m.IsFocused = &b
rule.MatchIsFocused = &b
}
if val, ok := node.Properties.Get("is-active-in-column"); ok {
b := val.ValueString() == "true"
m.IsActiveInColumn = &b
rule.MatchIsActiveInColumn = &b
}
if val, ok := node.Properties.Get("is-window-cast-target"); ok {
b := val.ValueString() == "true"
m.IsWindowCastTarget = &b
rule.MatchIsWindowCastTarget = &b
}
if val, ok := node.Properties.Get("is-urgent"); ok {
b := val.ValueString() == "true"
m.IsUrgent = &b
rule.MatchIsUrgent = &b
}
if val, ok := node.Properties.Get("at-startup"); ok {
b := val.ValueString() == "true"
m.AtStartup = &b
rule.MatchAtStartup = &b
}
return m
}
func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowRule) {
@@ -419,45 +385,6 @@ func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowR
}
}
func (p *NiriRulesParser) parseBackgroundEffectNode(node *document.Node, rule *NiriWindowRule) {
if node.Children == nil {
return
}
for _, child := range node.Children {
switch child.Name.String() {
case "blur":
b := p.parseBoolArg(child)
rule.BgBlur = &b
case "xray":
b := p.parseBoolArg(child)
rule.BgXray = &b
case "noise":
if f, ok := p.parseFloatArg(child); ok {
rule.BgNoise = &f
}
case "saturation":
if f, ok := p.parseFloatArg(child); ok {
rule.BgSaturation = &f
}
}
}
}
func (p *NiriRulesParser) parseFloatArg(node *document.Node) (float64, bool) {
if len(node.Arguments) == 0 {
return 0, false
}
val := node.Arguments[0].ResolvedValue()
switch v := val.(type) {
case float64:
return v, true
case int64:
return float64(v), true
}
return 0, false
}
func (p *NiriRulesParser) parseFocusRingNode(node *document.Node, rule *NiriWindowRule) {
if node.Children == nil {
return
@@ -534,27 +461,6 @@ func ParseNiriWindowRules(configDir string) (*NiriRulesParseResult, error) {
}, nil
}
func convertNiriMatches(matches []NiriMatch) []windowrules.MatchCriteria {
if len(matches) == 0 {
return nil
}
result := make([]windowrules.MatchCriteria, 0, len(matches))
for _, m := range matches {
result = append(result, windowrules.MatchCriteria{
AppID: m.AppID,
Title: m.Title,
IsFloating: m.IsFloating,
IsActive: m.IsActive,
IsFocused: m.IsFocused,
IsActiveInColumn: m.IsActiveInColumn,
IsWindowCastTarget: m.IsWindowCastTarget,
IsUrgent: m.IsUrgent,
AtStartup: m.AtStartup,
})
}
return result
}
func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.WindowRule {
result := make([]windowrules.WindowRule, 0, len(niriRules))
for i, nr := range niriRules {
@@ -573,7 +479,6 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win
IsUrgent: nr.MatchIsUrgent,
AtStartup: nr.MatchAtStartup,
},
Matches: convertNiriMatches(nr.Matches),
Actions: windowrules.Actions{
Opacity: nr.Opacity,
OpenFloating: nr.OpenFloating,
@@ -601,10 +506,6 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win
FocusRingOff: nr.FocusRingOff,
BorderOff: nr.BorderOff,
DrawBorderWithBg: nr.DrawBorderWithBg,
BackgroundBlur: nr.BgBlur,
BackgroundXray: nr.BgXray,
BackgroundNoise: nr.BgNoise,
BackgroundSaturation: nr.BgSaturation,
},
}
result = append(result, wr)
@@ -783,7 +684,6 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error)
IsUrgent: nr.MatchIsUrgent,
AtStartup: nr.MatchAtStartup,
},
Matches: convertNiriMatches(nr.Matches),
Actions: windowrules.Actions{
Opacity: nr.Opacity,
OpenFloating: nr.OpenFloating,
@@ -811,10 +711,6 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error)
FocusRingOff: nr.FocusRingOff,
BorderOff: nr.BorderOff,
DrawBorderWithBg: nr.DrawBorderWithBg,
BackgroundBlur: nr.BgBlur,
BackgroundXray: nr.BgXray,
BackgroundNoise: nr.BgNoise,
BackgroundSaturation: nr.BgSaturation,
},
}
@@ -844,54 +740,44 @@ func (p *NiriWritableProvider) writeDMSRules(rules []windowrules.WindowRule) err
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
}
func formatNiriMatchLine(m windowrules.MatchCriteria) (string, bool) {
var matchProps []string
if m.AppID != "" {
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
}
if m.Title != "" {
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
}
if m.IsFloating != nil {
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
}
if m.IsActive != nil {
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
}
if m.IsFocused != nil {
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
}
if m.IsActiveInColumn != nil {
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
}
if m.IsWindowCastTarget != nil {
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
}
if m.IsUrgent != nil {
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
}
if m.AtStartup != nil {
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
}
if len(matchProps) == 0 {
return "", false
}
return " match " + strings.Join(matchProps, " "), true
}
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
var lines []string
lines = append(lines, fmt.Sprintf("// @id=%s @name=%s", rule.ID, rule.Name))
lines = append(lines, "window-rule {")
matches := rule.Matches
if len(matches) == 0 {
matches = []windowrules.MatchCriteria{rule.MatchCriteria}
}
for _, m := range matches {
if line, ok := formatNiriMatchLine(m); ok {
lines = append(lines, line)
m := rule.MatchCriteria
if m.AppID != "" || m.Title != "" || m.IsFloating != nil || m.IsActive != nil ||
m.IsFocused != nil || m.IsActiveInColumn != nil || m.IsWindowCastTarget != nil ||
m.IsUrgent != nil || m.AtStartup != nil {
var matchProps []string
if m.AppID != "" {
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
}
if m.Title != "" {
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
}
if m.IsFloating != nil {
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
}
if m.IsActive != nil {
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
}
if m.IsFocused != nil {
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
}
if m.IsActiveInColumn != nil {
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
}
if m.IsWindowCastTarget != nil {
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
}
if m.IsUrgent != nil {
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
}
if m.AtStartup != nil {
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
}
lines = append(lines, " match "+strings.Join(matchProps, " "))
}
a := rule.Actions
@@ -972,31 +858,10 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
lines = append(lines, fmt.Sprintf(" draw-border-with-background %t", *a.DrawBorderWithBg))
}
if a.BackgroundBlur != nil || a.BackgroundXray != nil || a.BackgroundNoise != nil || a.BackgroundSaturation != nil {
lines = append(lines, " background-effect {")
if a.BackgroundBlur != nil {
lines = append(lines, fmt.Sprintf(" blur %t", *a.BackgroundBlur))
}
if a.BackgroundXray != nil {
lines = append(lines, fmt.Sprintf(" xray %t", *a.BackgroundXray))
}
if a.BackgroundNoise != nil {
lines = append(lines, fmt.Sprintf(" noise %s", formatFloat(*a.BackgroundNoise)))
}
if a.BackgroundSaturation != nil {
lines = append(lines, fmt.Sprintf(" saturation %s", formatFloat(*a.BackgroundSaturation)))
}
lines = append(lines, " }")
}
lines = append(lines, "}")
return strings.Join(lines, "\n")
}
func formatFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}
func formatSizeProperty(name, value string) string {
parts := strings.SplitN(value, " ", 2)
if len(parts) == 2 {
+6 -13
View File
@@ -43,10 +43,6 @@ type Actions struct {
FocusRingOff *bool `json:"focusRingOff,omitempty"`
BorderOff *bool `json:"borderOff,omitempty"`
DrawBorderWithBg *bool `json:"drawBorderWithBackground,omitempty"`
BackgroundBlur *bool `json:"backgroundBlur,omitempty"`
BackgroundXray *bool `json:"backgroundXray,omitempty"`
BackgroundNoise *float64 `json:"backgroundNoise,omitempty"`
BackgroundSaturation *float64 `json:"backgroundSaturation,omitempty"`
Size string `json:"size,omitempty"`
Move string `json:"move,omitempty"`
Monitor string `json:"monitor,omitempty"`
@@ -66,13 +62,12 @@ type Actions struct {
}
type WindowRule struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Enabled bool `json:"enabled"`
MatchCriteria MatchCriteria `json:"matchCriteria"`
Matches []MatchCriteria `json:"matches,omitempty"`
Actions Actions `json:"actions"`
Source string `json:"source,omitempty"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Enabled bool `json:"enabled"`
MatchCriteria MatchCriteria `json:"matchCriteria"`
Actions Actions `json:"actions"`
Source string `json:"source,omitempty"`
}
type DMSRulesStatus struct {
@@ -84,8 +79,6 @@ type DMSRulesStatus struct {
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
ConfigFormat string `json:"configFormat,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
}
type RuleSet struct {
-7
View File
@@ -48,13 +48,6 @@ fragments.
keyboard shortcuts in `dms/binds-user.lua`, or use the Keyboard Shortcuts page in
DMS Settings.
Stock configs include a 3-finger horizontal touchpad gesture for workspace
switching (`hl.gesture` in `dms/binds.lua`) and basic touchpad settings
(`tap_to_click`, `natural_scroll` in `hyprland.lua`). To customize or disable
gestures, add your own `hl.gesture(...)` lines to `dms/binds-user.lua`, or unset
a stock gesture with `action = "unset"` matching the original fingers,
direction, and modifiers.
Most other existing non-empty Lua fragments are preserved.
## Legacy Config Migration
-63
View File
@@ -282,53 +282,6 @@ dms ipc call inhibit toggle
dms ipc call inhibit enable
```
## Target: `powerprofile`
Power profile control via `power-profiles-daemon`. Changes stay in sync with DMS UI and trigger the power profile OSD when enabled.
Requires `power-profiles-daemon` to be installed and running. Works on all compositors.
### Functions
**`open`**
- Show the power profile picker modal
- Returns: Success confirmation or error if daemon unavailable
**`close`**
- Close the power profile picker modal
- Returns: Success confirmation
**`toggle`**
- Toggle power profile picker modal visibility
- Returns: Success confirmation or error if daemon unavailable
**`list`**
- List available profile slugs, one per line
- Returns: `power-saver`, `balanced`, and `performance` when supported
**`status`**
- Get the currently active profile slug
- Returns: `power-saver`, `balanced`, `performance`, or error if daemon unavailable
**`set <profile>`**
- Set the active power profile
- Parameters: Profile slug or alias — `power-saver` (`powersaver`, `saver`, `0`), `balanced` (`1`), `performance` (`2`)
- Returns: Success confirmation or error if profile unknown, unsupported, or write failed
**`cycle`**
- Cycle to the next available profile in order: power-saver → balanced → performance → power-saver
- Returns: Success confirmation or error if daemon unavailable or write failed
### Examples
```bash
dms ipc call powerprofile status
dms ipc call powerprofile list
dms ipc call powerprofile cycle
dms ipc call powerprofile set balanced
dms ipc call powerprofile set performance
dms ipc call powerprofile toggle
```
## Target: `wallpaper`
Wallpaper management and retrieval with support for per-monitor configurations.
@@ -590,18 +543,6 @@ Power menu modal control for system power actions.
- `close` - Hide power menu modal
- `toggle` - Toggle power menu modal visibility
### Target: `powerprofile`
Power profile picker modal and profile control via `power-profiles-daemon`.
**Functions:**
- `open` - Show power profile picker modal
- `close` - Hide power profile picker modal
- `toggle` - Toggle power profile picker modal visibility
- `list` - List available profile slugs
- `status` - Get current profile slug
- `set <profile>` - Set profile by slug or alias (`power-saver`, `balanced`, `performance`)
- `cycle` - Cycle to the next available profile
### Target: `control-center`
Control Center popout containing network, bluetooth, audio, power, and other quick settings.
@@ -732,10 +673,6 @@ dms ipc call processlist toggle
# Show power menu
dms ipc call powermenu toggle
# Cycle or set power profile (requires power-profiles-daemon)
dms ipc call powerprofile cycle
dms ipc call powerprofile toggle
# Open notepad
dms ipc call notepad toggle
-10
View File
@@ -57,15 +57,9 @@ const KEY_MAP = {
16842802: "XF86Eject",
16842791: "XF86Calculator",
16842806: "XF86Explorer",
16777360: "XF86HomePage",
16842794: "XF86HomePage",
16777362: "XF86Search",
16777426: "XF86Search",
16777376: "XF86Mail",
16777427: "XF86Mail",
16777377: "XF86AudioMedia",
16777419: "XF86Calculator",
16777429: "XF86Explorer",
16777442: "XF86Launch0",
16777443: "XF86Launch1",
33: "1",
@@ -135,10 +129,6 @@ function xkbKeyFromQtKey(qk) {
return String.fromCharCode(qk);
if (qk >= 16777264 && qk <= 16777298)
return "F" + (qk - 16777264 + 1);
if (qk >= 16777378 && qk <= 16777387)
return "XF86Launch" + (qk - 16777378);
if (qk >= 16777388 && qk <= 16777393)
return "XF86Launch" + String.fromCharCode(65 + qk - 16777388);
return KEY_MAP[qk] || "";
}
-24
View File
@@ -770,26 +770,6 @@ const DMS_ACTION_ARGS = {
}
};
const DMS_AMOUNT_LABELS = {
"audio increment": "Volume Up",
"audio decrement": "Volume Down",
"mpris increment": "Player Volume Up",
"mpris decrement": "Player Volume Down",
"brightness increment": "Brightness Up",
"brightness decrement": "Brightness Down"
};
function getDmsAmountLabel(action) {
var parsed = parseDmsActionArgs(action);
var label = DMS_AMOUNT_LABELS[parsed.base];
if (!label)
return null;
var amount = parsed.args?.amount;
if (amount === undefined || amount === null || amount === "")
return label;
return label + " (" + amount + "%)";
}
function getActionTypes() {
return ACTION_TYPES;
}
@@ -864,10 +844,6 @@ function getActionLabel(action, compositor) {
if (!action)
return "";
var amountLabel = getDmsAmountLabel(action);
if (amountLabel)
return amountLabel;
var dmsAct = findDmsAction(action);
if (dmsAct)
return dmsAct.label;
-91
View File
@@ -1,91 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("LayerShell")
function _toLayer(name) {
switch (name) {
case "background":
return WlrLayer.Background;
case "bottom":
return WlrLayer.Bottom;
case "top":
return WlrLayer.Top;
case "overlay":
return WlrLayer.Overlay;
}
return undefined;
}
function _toName(layer) {
switch (layer) {
case WlrLayer.Background:
return "background";
case WlrLayer.Bottom:
return "bottom";
case WlrLayer.Top:
return "top";
case WlrLayer.Overlay:
return "overlay";
}
return "top";
}
// Resolve a WlrLayer from a DMS_*_LAYER env override.
// name: env var to read, e.g. "DMS_OSD_LAYER"
// fallback: WlrLayer used when the var is unset or unrecognized
// opts (optional):
// allow: array of honored layer names; recognized names outside it
// are treated as invalid
// invalidLayer: WlrLayer used for a recognized-but-disallowed value
// (default: fallback)
// label: context for the diagnostic, e.g. "OSDs"; omit to stay silent
// error: log at error level instead of warn
function fromEnv(name, fallback, opts) {
const value = Quickshell.env(name);
if (!value)
return fallback;
const requested = _toLayer(value);
if (requested === undefined)
return fallback;
const allow = opts?.allow;
if (!allow || allow.indexOf(value) !== -1)
return requested;
const invalid = opts?.invalidLayer ?? fallback;
if (opts?.label) {
const msg = `'${value}' layer is not valid for ${opts.label}. Defaulting to '${_toName(invalid)}' layer.`;
if (opts?.error)
log.error(msg);
else
log.warn(msg);
}
return invalid;
}
// For call sites that only need "is the override the overlay layer?".
// Honors "overlay" (true) and bottom/background/top (false); anything else
// returns `fallback`.
function envUsesOverlay(name, fallback) {
switch (Quickshell.env(name)) {
case "overlay":
return true;
case "bottom":
case "background":
case "top":
return false;
default:
return fallback;
}
}
}
-1
View File
@@ -13,7 +13,6 @@ Singleton {
property var currentModalsByScreen: ({})
function openModal(modal) {
PopoutManager.screenshotActive = false;
const screenName = modal.effectiveScreen?.name ?? "unknown";
currentModalsByScreen[screenName] = modal;
modalChanged();
-5
View File
@@ -10,9 +10,6 @@ Singleton {
property var currentPopoutsByScreen: ({})
property var currentPopoutTriggers: ({})
// Set by the screenshot IPC handshake (dms screenshot region select); cleared by end() or any popout/modal open.
property bool screenshotActive: false
signal popoutOpening
signal popoutChanged
@@ -50,7 +47,6 @@ Singleton {
function showPopout(popout) {
if (!popout || !popout.screen)
return;
screenshotActive = false;
popoutOpening();
const screenName = popout.screen.name;
@@ -101,7 +97,6 @@ Singleton {
function requestPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen)
return;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
+4 -34
View File
@@ -154,8 +154,6 @@ Singleton {
property var trayItemOrder: []
property var recentColors: []
property bool showThirdPartyPlugins: false
property bool pluginBrowserInstalledFirst: false
property string pluginBrowserSortMode: "default"
property string launchPrefix: ""
property string lastBrightnessDevice: ""
property var brightnessExponentialDevices: ({})
@@ -966,20 +964,6 @@ Singleton {
saveSettings();
}
function setPluginBrowserInstalledFirst(enabled) {
pluginBrowserInstalledFirst = enabled;
saveSettings();
}
function setPluginBrowserSortMode(mode) {
if (mode === "type" || mode === "contributor")
mode = "author";
if (mode !== "default" && mode !== "name" && mode !== "author" && mode !== "category")
mode = "default";
pluginBrowserSortMode = mode;
saveSettings();
}
function setLaunchPrefix(prefix) {
launchPrefix = prefix;
saveSettings();
@@ -1369,27 +1353,13 @@ 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: root.greeterSessionBaseDir ? (root.greeterSessionBaseDir + "/session.json") : ""
path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/session.json";
}
preload: isGreeterMode
blockLoading: false
blockWrites: true
+1 -15
View File
@@ -315,8 +315,6 @@ Singleton {
property bool controlCenterShowBatteryIcon: false
property bool controlCenterShowPrinterIcon: false
property bool controlCenterShowScreenSharingIcon: true
property bool controlCenterShowIdleInhibitorIcon: false
property bool controlCenterShowDoNotDisturbIcon: false
property bool showPrivacyButton: true
property bool privacyShowMicIcon: false
property bool privacyShowCameraIcon: false
@@ -407,7 +405,6 @@ Singleton {
property int appsDockEnlargePercentage: 125
property int appsDockIconSizePercentage: 100
property bool keyboardLayoutNameCompactMode: false
property bool keyboardLayoutNameShowIcon: false
property bool runningAppsCurrentWorkspace: true
property bool runningAppsGroupByApp: false
property bool runningAppsCurrentMonitor: false
@@ -417,7 +414,6 @@ Singleton {
property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterAutoLogin: false
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
@@ -1337,15 +1333,6 @@ Singleton {
});
}
function scheduleGreeterAutoLoginSync() {
if (isGreeterMode)
return;
Qt.callLater(() => {
Processes.settingsRoot = root;
Processes.scheduleGreeterAutoLoginSync();
});
}
readonly property var _hooks: ({
"applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes,
@@ -1353,8 +1340,7 @@ Singleton {
"applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor,
"scheduleAuthApply": scheduleAuthApply,
"scheduleGreeterAutoLoginSync": scheduleGreeterAutoLoginSync
"scheduleAuthApply": scheduleAuthApply
})
function set(key, value) {
+3 -21
View File
@@ -970,7 +970,6 @@ Singleton {
readonly property int shorterDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.shorter
readonly property int shortDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.short
readonly property bool snapListModelChanges: shortDuration <= 0
readonly property int mediumDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.medium
readonly property int longDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.long
readonly property int extraLongDuration: (typeof SettingsData !== "undefined" && SettingsData.animationSpeed === SettingsData.AnimationSpeed.Custom) ? SettingsData.customAnimationDuration : currentDurations.extraLong
@@ -2080,29 +2079,12 @@ 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 {
id: dynamicColorsFileView
path: {
if (SessionData.isGreeterMode)
return root.greeterColorsBaseDir ? (root.greeterColorsBaseDir + "/colors.json") : "";
return stateDir + "/dms-colors.json";
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath;
}
blockLoading: false
watchChanges: !SessionData.isGreeterMode
-165
View File
@@ -12,35 +12,6 @@ Singleton {
property var settingsRoot: null
onSettingsRootChanged: {
if (settingsRoot && !settingsRoot.isGreeterMode)
consumeGreeterAutoLoginPendingSync();
}
readonly property string greeterAutoLoginPendingSyncPath: (Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending"
function consumeGreeterAutoLoginPendingSync() {
if (!settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginPendingCheckProcess.running = true;
}
property var greeterAutoLoginPendingCheckProcess: Process {
command: ["sh", "-c", "if [ -f " + JSON.stringify(root.greeterAutoLoginPendingSyncPath) + " ]; then rm -f " + JSON.stringify(root.greeterAutoLoginPendingSyncPath) + "; echo pending; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if ((text || "").trim() !== "pending" || !root.settingsRoot)
return;
if (!root.settingsRoot.greeterAutoLogin)
root.settingsRoot.set("greeterAutoLogin", true);
else
root.scheduleGreeterAutoLoginSync();
}
}
}
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
@@ -325,66 +296,6 @@ Singleton {
authApplyDebounce.restart();
}
// --- Greeter auto-login sync pipeline ---
property bool greeterAutoLoginSyncRunning: false
property bool greeterAutoLoginSyncQueued: false
property bool greeterAutoLoginSyncRerunRequested: false
property string greeterAutoLoginSyncStdout: ""
property string greeterAutoLoginSyncStderr: ""
property string greeterAutoLoginSyncTerminalFallbackStderr: ""
function scheduleGreeterAutoLoginSync() {
if (!settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginSyncQueued = true;
if (greeterAutoLoginSyncRunning) {
greeterAutoLoginSyncRerunRequested = true;
return;
}
greeterAutoLoginSyncDebounce.restart();
}
function beginGreeterAutoLoginSync() {
if (!greeterAutoLoginSyncQueued || greeterAutoLoginSyncRunning || !settingsRoot || settingsRoot.isGreeterMode)
return;
greeterAutoLoginSyncQueued = false;
greeterAutoLoginSyncRerunRequested = false;
greeterAutoLoginSyncStdout = "";
greeterAutoLoginSyncStderr = "";
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncRunning = true;
greeterAutoLoginSyncSudoProbeProcess.running = true;
}
function launchGreeterAutoLoginSyncTerminalFallback(details) {
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin-only", "greeter-autologin-sync");
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncTerminalFallbackProcess.running = true;
}
function greeterAutoLoginSyncSuccessToast(details) {
const enabling = settingsRoot && settingsRoot.greeterAutoLogin;
// Clear the sticky in-progress toast, then confirm with an auto-dismissing toast.
ToastService.dismissCategory("greeter-autologin-sync");
if (enabling) {
ToastService.showWarning(I18n.tr("Auto-login enabled"), I18n.tr("You'll skip the greeter password after the next reboot. The lock screen and signing out still require your password.") + (details ? "\n\n" + details : ""));
} else {
ToastService.showInfo(I18n.tr("Auto-login disabled"), I18n.tr("You'll enter your password at the greeter after the next reboot.") + (details ? "\n\n" + details : ""));
}
}
function finishGreeterAutoLoginSync() {
const shouldRerun = greeterAutoLoginSyncQueued || greeterAutoLoginSyncRerunRequested;
greeterAutoLoginSyncRunning = false;
greeterAutoLoginSyncRerunRequested = false;
if (shouldRerun)
greeterAutoLoginSyncDebounce.restart();
}
// --- PAM parsing helpers ---
function stripPamComment(line) {
@@ -522,82 +433,6 @@ Singleton {
onTriggered: root.beginAuthApply()
}
Timer {
id: greeterAutoLoginSyncDebounce
interval: 300
repeat: false
onTriggered: root.beginGreeterAutoLoginSync()
}
property var greeterAutoLoginSyncProcess: Process {
command: ["dms", "greeter", "sync", "--yes", "--autologin-only"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncStdout = text || ""
}
stderr: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncStderr = text || ""
}
onExited: exitCode => {
const out = (root.greeterAutoLoginSyncStdout || "").trim();
const err = (root.greeterAutoLoginSyncStderr || "").trim();
if (exitCode === 0) {
let details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
root.greeterAutoLoginSyncSuccessToast(details);
root.finishGreeterAutoLoginSync();
return;
}
let details = "";
if (out !== "")
details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
root.launchGreeterAutoLoginSyncTerminalFallback(details);
}
}
property var greeterAutoLoginSyncSudoProbeProcess: Process {
command: ["sudo", "-n", "true"]
running: false
onExited: exitCode => {
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
if (exitCode === 0) {
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup…") : I18n.tr("Disabling auto-login on startup…"), "", "dms greeter sync --autologin-only", "greeter-autologin-sync");
root.greeterAutoLoginSyncProcess.running = true;
return;
}
root.launchGreeterAutoLoginSyncTerminalFallback();
}
}
property var greeterAutoLoginSyncTerminalFallbackProcess: Process {
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin-only"]
running: false
stderr: StdioCollector {
onStreamFinished: root.greeterAutoLoginSyncTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
if (exitCode === 0) {
root.greeterAutoLoginSyncSuccessToast("");
} else {
let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin-only", "greeter-autologin-sync");
}
root.finishGreeterAutoLoginSync();
}
}
property var authApplyProcess: Process {
command: ["dms", "auth", "sync", "--yes"]
running: false
@@ -56,8 +56,6 @@ var SPEC = {
trayItemOrder: { def: [] },
recentColors: { def: [] },
showThirdPartyPlugins: { def: false },
pluginBrowserInstalledFirst: { def: false },
pluginBrowserSortMode: { def: "default" },
launchPrefix: { def: "" },
lastBrightnessDevice: { def: "" },
@@ -104,8 +104,6 @@ var SPEC = {
controlCenterShowBatteryIcon: { def: false },
controlCenterShowPrinterIcon: { def: false },
controlCenterShowScreenSharingIcon: { def: true },
controlCenterShowIdleInhibitorIcon: { def: false },
controlCenterShowDoNotDisturbIcon: { def: false },
showPrivacyButton: { def: true },
privacyShowMicIcon: { def: false },
@@ -167,7 +165,6 @@ var SPEC = {
appsDockEnlargePercentage: { def: 125 },
appsDockIconSizePercentage: { def: 100 },
keyboardLayoutNameCompactMode: { def: false },
keyboardLayoutNameShowIcon: { def: false},
runningAppsCurrentWorkspace: { def: true },
runningAppsGroupByApp: { def: false },
runningAppsCurrentMonitor: { def: false },
@@ -185,7 +182,6 @@ var SPEC = {
lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterAutoLogin: { def: false, onChange: "scheduleGreeterAutoLoginSync" },
greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
greeterWallpaperPath: { def: "" },
-18
View File
@@ -1185,24 +1185,6 @@ Item {
}
}
LazyLoader {
id: powerProfileModalLoader
active: false
PowerProfileModal {
id: powerProfileModal
Component.onCompleted: {
PopoutService.powerProfileModal = powerProfileModal;
}
}
Component.onCompleted: {
PopoutService.powerProfileModalLoader = powerProfileModalLoader;
}
}
DMSShellIPC {
powerMenuModalLoader: powerMenuModalLoader
processListModalLoader: processListModalLoader
-85
View File
@@ -3,7 +3,6 @@ import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import qs.Modules.Settings.DisplayConfig
@@ -162,21 +161,6 @@ Item {
target: "control-center"
}
IpcHandler {
// Screenshot region-select handshake
function begin(): string {
PopoutManager.screenshotActive = true;
return "SCREENSHOT_MODE_ON";
}
function end(): string {
PopoutManager.screenshotActive = false;
return "SCREENSHOT_MODE_OFF";
}
target: "screenshot"
}
IpcHandler {
function resolveTabIndex(tab: string): int {
switch ((tab || "").toLowerCase()) {
@@ -1891,73 +1875,4 @@ Item {
target: "tray"
}
IpcHandler {
function open(): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
PopoutService.openPowerProfileModal();
return "POWERPROFILE_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closePowerProfileModal();
return "POWERPROFILE_CLOSE_SUCCESS";
}
function toggle(): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
PopoutService.togglePowerProfileModal();
return "POWERPROFILE_TOGGLE_SUCCESS";
}
function list(): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
return PowerProfileWatcher.availableProfiles.map(profile => PowerProfileWatcher.profileSlug(profile)).join("\n");
}
function status(): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
return PowerProfileWatcher.profileSlug(PowerProfiles.profile);
}
function set(profile: string): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
if (!profile)
return "ERROR: No profile specified";
const parsed = PowerProfileWatcher.parseProfileSlug(profile);
if (parsed === -1)
return "ERROR: Unknown power profile. Supported options: power-saver, balanced, performance";
if (parsed === PowerProfile.Performance && !PowerProfiles.hasPerformanceProfile)
return "ERROR: Performance profile not supported by hardware";
if (!PowerProfileWatcher.applyProfile(parsed))
return "ERROR: Failed to set power profile";
return "POWERPROFILE_SET_SUCCESS";
}
function cycle(): string {
if (!PowerProfileWatcher.available)
return "ERROR: power-profiles-daemon not available";
if (!PowerProfileWatcher.cycleProfile())
return "ERROR: Failed to set power profile";
return "POWERPROFILE_CYCLE_SUCCESS";
}
target: "powerprofile"
}
}
@@ -26,8 +26,7 @@ Item {
ClipboardHeader {
id: header
width: parent.width
recentsCount: modal.unpinnedEntries.length
savedCount: modal.pinnedEntries.length
totalCount: modal.totalCount
showKeyboardHints: modal.showKeyboardHints
activeTab: modal.activeTab
pinnedCount: modal.pinnedCount
@@ -66,6 +65,15 @@ Item {
forceActiveFocus();
});
}
Connections {
target: modal
function onOpened() {
Qt.callLater(function () {
searchField.forceActiveFocus();
});
}
}
}
}
@@ -100,20 +108,6 @@ Item {
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
states: [
State {
name: "snap"
when: Theme.snapListModelChanges
PropertyChanges {
target: clipboardListView
add: null
remove: null
displaced: null
move: null
}
}
]
function ensureVisible(index) {
if (index < 0 || index >= count) {
return;
@@ -174,20 +168,6 @@ Item {
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
states: [
State {
name: "snap"
when: Theme.snapListModelChanges
PropertyChanges {
target: savedListView
add: null
remove: null
displaced: null
move: null
}
}
]
function ensureVisible(index) {
if (index < 0 || index >= count) {
return;
+22 -33
View File
@@ -29,29 +29,32 @@ Item {
}
try {
const decoded = Qt.atob(sanitized);
if (!decoded) {
return data;
const chars = new Array(sanitized.length);
for (let i = 0; i < sanitized.length; i++) {
chars[i] = sanitized.charAt(i);
}
let binary = "";
if (typeof decoded === "string") {
// Pre-6.11 Qt.atob returns a binary string directly
binary = decoded;
} else {
// Qt 6.11+ Qt.atob returns an ArrayBuffer convert to avoid O(n²) concat/stack limits
const bytes = new Uint8Array(decoded);
const chunkSize = 8192;
const chunks = [];
for (let i = 0; i < bytes.length; i += chunkSize) {
chunks.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)));
let buffer = null;
if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
buffer = Qt.atob(chars);
} else if (typeof atob === "function") {
const binary = atob(sanitized);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
binary = chunks.join("");
buffer = bytes.buffer;
}
if (!binary) {
if (!buffer || buffer.byteLength === 0) {
return data;
}
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
try {
return decodeURIComponent(escape(binary));
} catch (e) {
@@ -71,7 +74,6 @@ Item {
Qt.callLater(function () {
if (editField) {
editField.forceActiveFocus();
editField.cursorPosition = editField.text.length;
}
});
@@ -89,10 +91,6 @@ Item {
if (!root.entry || root.entry.id !== requestedId) {
return;
}
if (!response.result) {
ClipboardService.refresh();
return;
}
const result = response.result;
let fullText = "";
if (result?.data) {
@@ -106,17 +104,7 @@ Item {
}
root.editorText = fullText;
if (editField) {
if (fullText.length > 50000) {
Qt.callLater(function () {
if (editField) {
editField.text = fullText;
editField.cursorPosition = fullText.length;
}
});
} else {
editField.text = fullText;
editField.cursorPosition = fullText.length;
}
editField.text = fullText;
}
});
}
@@ -264,6 +252,7 @@ Item {
id: editField
width: editScroll.width
height: Math.max(editScroll.height, contentHeight)
text: root.editorText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: TextEdit.Wrap
@@ -78,9 +78,10 @@ Rectangle {
onClicked: {
if (entryType === "image") {
return;
// TODO - forward to editing software
} else {
editRequested();
}
editRequested();
}
}
@@ -6,8 +6,7 @@ import qs.Modals.Clipboard
Item {
id: header
property int recentsCount: 0
property int savedCount: 0
property int totalCount: 0
property bool showKeyboardHints: false
property string activeTab: "recents"
property int pinnedCount: 0
@@ -32,7 +31,7 @@ Item {
}
StyledText {
text: (header.activeTab === "saved" ? I18n.tr("Clipboard Saved") : I18n.tr("Clipboard History")) + ` (${header.activeTab === "saved" ? header.savedCount : header.recentsCount})`
text: I18n.tr("Clipboard History") + ` (${totalCount})`
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
@@ -49,7 +48,6 @@ Item {
iconName: "push_pin"
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
backgroundColor: header.activeTab === "saved" ? Theme.primarySelected : "transparent"
visible: header.pinnedCount > 0
tooltipText: header.activeTab === "saved" ? I18n.tr("Recent") : I18n.tr("Saved")
onClicked: tabChanged(header.activeTab === "saved" ? "recents" : "saved")
@@ -1,210 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
FocusScope {
id: root
property var clearConfirmDialog: null
property string activeTab: "recents"
property bool showKeyboardHints: false
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
property string mode: "history"
property string searchText: ClipboardService.searchText
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
readonly property var modalFocusScope: root
property alias searchField: historyContent.searchField
property alias editorView: editorView
property alias keyboardController: keyboardController
signal closeRequested
signal instantCloseRequested
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
onSearchTextChanged: ClipboardService.searchText = searchText
function hide() {
closeRequested();
}
function pasteSelected() {
ClipboardService.pasteSelected(() => root.instantCloseRequested());
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, () => root.closeRequested());
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function refreshClipboard() {
ClipboardService.refresh();
}
function editEntry(entry) {
if (!entry || entry.isImage) {
return;
}
editorView.setEntry(entry);
mode = "editor";
}
function resetState() {
activeImageLoads = 0;
mode = "history";
ClipboardService.reset();
keyboardController.reset();
}
focus: true
Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
ClipboardKeyboardController {
id: keyboardController
modal: root
}
Item {
id: historyView
anchors.fill: parent
opacity: 1
scale: 1
visible: opacity > 0.01
enabled: root.mode === "history"
ClipboardContent {
id: historyContent
anchors.fill: parent
modal: root
clearConfirmDialog: root.clearConfirmDialog
}
}
ClipboardEditor {
id: editorView
anchors.fill: parent
opacity: 0
scale: 0.98
visible: opacity > 0.01
enabled: root.mode === "editor"
focus: root.mode === "editor"
modal: root
keyController: keyboardController
}
states: [
State {
name: "history"
when: root.mode === "history"
PropertyChanges {
target: historyView
opacity: 1
scale: 1
}
PropertyChanges {
target: editorView
opacity: 0
scale: 0.98
}
},
State {
name: "editor"
when: root.mode === "editor"
PropertyChanges {
target: historyView
opacity: 0
scale: 0.98
}
PropertyChanges {
target: editorView
opacity: 1
scale: 1
}
}
]
transitions: [
Transition {
from: "history"
to: "editor"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
},
Transition {
from: "editor"
to: "history"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
}
@@ -17,28 +17,74 @@ DankModal {
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
}
property string activeTab: "recents"
onActiveTabChanged: {
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
property bool showKeyboardHints: false
property Component clipboardContent
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
Ref {
service: ClipboardService
}
property string mode: "history"
onModeChanged: {
if (mode !== "history") {
return;
}
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.forceActiveFocus();
}
});
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
function toggle() {
if (shouldBeVisible) {
hide();
return;
} else {
show();
}
show();
}
function show() {
open();
mode = "history";
activeImageLoads = 0;
shouldHaveFocus = true;
ClipboardService.reset();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.resetState();
}
if (clipboardHistoryModal.clipboardAvailable) {
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible) {
if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh();
}
});
} else {
ClipboardService.refresh();
@@ -56,12 +102,61 @@ DankModal {
}
onDialogClosed: {
if (contentLoader.item) {
contentLoader.item.resetState();
}
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
function refreshClipboard() {
ClipboardService.refresh();
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
function editEntry(entry) {
if (!entry) {
return;
}
if (entry.isImage) {
return;
}
const editor = contentLoader.item?.editorView;
if (!editor) {
return;
}
editor.setEntry(entry);
mode = "editor";
}
visible: false
modalWidth: ClipboardConstants.modalWidth
@@ -71,11 +166,16 @@ DankModal {
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
closeOnEscapeKey: (contentLoader.item?.mode ?? "history") !== "editor"
closeOnEscapeKey: mode !== "editor"
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
content: clipboardContent
Ref {
service: ClipboardService
ClipboardKeyboardController {
id: keyboardController
modal: clipboardHistoryModal
}
ConfirmModal {
@@ -100,11 +200,112 @@ DankModal {
}
}
content: Component {
ClipboardHistoryContent {
clearConfirmDialog: clearConfirmDialog
onCloseRequested: clipboardHistoryModal.hide()
onInstantCloseRequested: clipboardHistoryModal.instantClose()
property var confirmDialog: clearConfirmDialog
clipboardContent: Component {
Item {
id: viewContainer
property alias editorView: editorView
property alias searchField: historyContent.searchField
anchors.fill: parent
Item {
id: historyView
anchors.fill: parent
opacity: 1
scale: 1
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "history"
ClipboardContent {
id: historyContent
anchors.fill: parent
modal: clipboardHistoryModal
clearConfirmDialog: clipboardHistoryModal.confirmDialog
}
}
ClipboardEditor {
id: editorView
anchors.fill: parent
opacity: 0
scale: 0.98
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "editor"
focus: clipboardHistoryModal.mode === "editor"
modal: clipboardHistoryModal
keyController: keyboardController
}
states: [
State {
name: "history"
when: clipboardHistoryModal.mode === "history"
PropertyChanges {
target: historyView
opacity: 1
scale: 1
}
PropertyChanges {
target: editorView
opacity: 0
scale: 0.98
}
},
State {
name: "editor"
when: clipboardHistoryModal.mode === "editor"
PropertyChanges {
target: historyView
opacity: 0
scale: 0.98
}
PropertyChanges {
target: editorView
opacity: 1
scale: 1
}
}
]
transitions: [
Transition {
from: "history"
to: "editor"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
},
Transition {
from: "editor"
to: "history"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
}
}
}
@@ -15,20 +15,47 @@ DankPopout {
property var parentWidget: null
property var triggerScreen: null
property string activeTab: "recents"
property bool showKeyboardHints: false
property int activeImageLoads: 0
readonly property int maxConcurrentLoads: 3
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
readonly property int totalCount: ClipboardService.totalCount
readonly property var clipboardEntries: ClipboardService.clipboardEntries
readonly property var pinnedEntries: ClipboardService.pinnedEntries
readonly property int pinnedCount: ClipboardService.pinnedCount
readonly property var confirmDialog: clearConfirmDialog
readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
readonly property int selectedIndex: ClipboardService.selectedIndex
readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
property string searchText: ClipboardService.searchText
onSearchTextChanged: ClipboardService.searchText = searchText
readonly property var modalFocusScope: contentLoader.item ?? null
Ref {
service: ClipboardService
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
function pasteSelected() {
ClipboardService.pasteSelected(instantClose);
}
function instantClose() {
close();
}
function show() {
open();
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.activeTab = activeTab;
contentLoader.item.resetState();
}
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
@@ -38,12 +65,47 @@ DankPopout {
function hide() {
close();
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
function refreshClipboard() {
ClipboardService.refresh();
}
function copyEntry(entry) {
ClipboardService.copyEntry(entry, hide);
}
function deleteEntry(entry) {
ClipboardService.deleteEntry(entry);
}
function deletePinnedEntry(entry) {
ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
}
function pinEntry(entry) {
ClipboardService.pinEntry(entry);
}
function unpinEntry(entry) {
ClipboardService.unpinEntry(entry);
}
function clearAll() {
ClipboardService.clearAll();
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
function getEntryType(entry) {
return ClipboardService.getEntryType(entry);
}
popupWidth: ClipboardConstants.popoutWidth
popupHeight: ClipboardConstants.popoutHeight
triggerWidth: 55
@@ -55,25 +117,20 @@ DankPopout {
onBackgroundClicked: hide()
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
if (!shouldBeVisible)
return;
}
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (root.shouldBeVisible) {
if (root.shouldBeVisible)
ClipboardService.refresh();
}
});
} else {
ClipboardService.refresh();
}
}
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item) {
contentLoader.item.activeTab = activeTab;
contentLoader.item.resetState();
}
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
@@ -82,13 +139,14 @@ DankPopout {
}
onPopoutClosed: {
if (contentLoader.item) {
contentLoader.item.resetState();
}
activeImageLoads = 0;
ClipboardService.reset();
keyboardController.reset();
}
Ref {
service: ClipboardService
ClipboardKeyboardController {
id: keyboardController
modal: root
}
ConfirmModal {
@@ -97,20 +155,48 @@ DankPopout {
confirmButtonColor: Theme.primary
}
property var confirmDialog: clearConfirmDialog
content: Component {
ClipboardHistoryContent {
FocusScope {
id: contentFocusScope
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
clearConfirmDialog: clearConfirmDialog
onCloseRequested: root.hide()
onInstantCloseRequested: root.close()
focus: true
property alias searchField: clipboardContentItem.searchField
Keys.onPressed: function (event) {
keyboardController.handleKey(event);
}
Component.onCompleted: {
activeTab = root.activeTab;
if (root.shouldBeVisible) {
if (root.shouldBeVisible)
forceActiveFocus();
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(() => contentFocusScope.forceActiveFocus());
}
}
function onOpened() {
Qt.callLater(() => {
if (clipboardContentItem.searchField) {
clipboardContentItem.searchField.forceActiveFocus();
}
});
}
}
ClipboardContent {
id: clipboardContentItem
modal: root
clearConfirmDialog: root.confirmDialog
}
}
}
@@ -13,7 +13,6 @@ Item {
required property var modal
required property var listView
required property int itemIndex
property bool disposed: false
Image {
id: thumbnailImage
@@ -21,13 +20,6 @@ Item {
property bool isVisible: false
property string cachedImageData: ""
property bool loadQueued: false
property bool activeLoad: false
property bool completed: false
property int loadGeneration: 0
property var activeEntryId: null
property var activeRequest: null
property var currentEntryId: entry && entry.id !== undefined ? entry.id : null
property string currentEntryType: entryType
anchors.fill: parent
source: cachedImageData ? `data:image/png;base64,${cachedImageData}` : ""
@@ -39,119 +31,29 @@ Item {
sourceSize.width: 128
sourceSize.height: 128
onCurrentEntryIdChanged: {
if (thumbnailImage.completed) {
thumbnailImage.resetForEntry();
}
}
onCurrentEntryTypeChanged: {
if (thumbnailImage.completed) {
thumbnailImage.resetForEntry();
}
}
function hasValidEntryId() {
return entry && entry.id !== undefined && entry.id !== null;
}
function releaseActiveLoad() {
if (!thumbnailImage.activeLoad) {
return;
}
thumbnailImage.activeLoad = false;
if (modal && modal.activeImageLoads > 0) {
modal.activeImageLoads--;
}
}
function finishLoad(request) {
thumbnailImage.loadQueued = false;
thumbnailImage.activeEntryId = null;
if (!request || thumbnailImage.activeRequest === request) {
thumbnailImage.activeRequest = null;
}
thumbnailImage.releaseActiveLoad();
}
function cancelLoad() {
if (thumbnailImage.activeRequest) {
thumbnailImage.activeRequest.cancelled = true;
thumbnailImage.activeRequest = null;
}
retryTimer.stop();
visibilityTimer.stop();
thumbnailImage.loadQueued = false;
thumbnailImage.activeEntryId = null;
thumbnailImage.releaseActiveLoad();
}
function resetForEntry() {
thumbnailImage.loadGeneration++;
thumbnailImage.cachedImageData = "";
thumbnailImage.isVisible = false;
thumbnailImage.cancelLoad();
Qt.callLater(function () {
if (thumbnail.disposed) {
return;
}
thumbnailImage.checkVisibility();
});
}
function startLoad() {
if (!modal) {
thumbnailImage.loadQueued = false;
return;
}
modal.activeImageLoads++;
thumbnailImage.activeLoad = true;
thumbnailImage.loadImage();
}
function tryLoadImage() {
if (thumbnail.disposed || thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData || !thumbnailImage.hasValidEntryId()) {
if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
return;
}
thumbnailImage.loadQueued = true;
if (modal && modal.activeImageLoads < modal.maxConcurrentLoads) {
thumbnailImage.startLoad();
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
modal.activeImageLoads++;
thumbnailImage.loadImage();
} else {
retryTimer.restart();
}
}
function loadImage() {
if (!thumbnailImage.hasValidEntryId()) {
thumbnailImage.finishLoad();
return;
}
const requestedId = entry.id;
const generation = thumbnailImage.loadGeneration;
const request = {
"cancelled": false
};
thumbnailImage.activeEntryId = requestedId;
thumbnailImage.activeRequest = request;
DMSService.sendRequest("clipboard.getEntry", {
"id": requestedId
"id": entry.id
}, function (response) {
if (request.cancelled) {
return;
}
if (thumbnail.disposed || generation !== thumbnailImage.loadGeneration || thumbnailImage.activeRequest !== request || thumbnailImage.activeEntryId !== requestedId) {
return;
}
thumbnailImage.finishLoad(request);
if (!entry || entry.id !== requestedId || entryType !== "image") {
return;
thumbnailImage.loadQueued = false;
if (modal.activeImageLoads > 0) {
modal.activeImageLoads--;
}
if (response.error) {
log.warn("Failed to load image:", requestedId);
return;
}
if (!response.result) {
ClipboardService.refresh();
log.warn("Failed to load image:", entry.id);
return;
}
const data = response.result?.data;
@@ -168,8 +70,9 @@ Item {
if (!thumbnailImage.loadQueued) {
return;
}
if (modal && modal.activeImageLoads < modal.maxConcurrentLoads) {
thumbnailImage.startLoad();
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
modal.activeImageLoads++;
thumbnailImage.loadImage();
} else {
retryTimer.restart();
}
@@ -177,8 +80,7 @@ Item {
}
Component.onCompleted: {
thumbnailImage.completed = true;
if (entryType !== "image" || listView.height <= 0 || !thumbnailImage.hasValidEntryId()) {
if (entryType !== "image" || listView.height <= 0) {
return;
}
@@ -192,11 +94,6 @@ Item {
}
}
Component.onDestruction: {
thumbnail.disposed = true;
thumbnailImage.cancelLoad();
}
Timer {
id: visibilityTimer
interval: 100
@@ -204,7 +101,7 @@ Item {
}
function checkVisibility() {
if (thumbnail.disposed || entryType !== "image" || listView.height <= 0 || isVisible || !thumbnailImage.hasValidEntryId()) {
if (entryType !== "image" || listView.height <= 0 || isVisible) {
return;
}
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
+23 -12
View File
@@ -262,7 +262,6 @@ Item {
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
opened();
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
@@ -498,12 +497,22 @@ Item {
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: root.useOverlayLayer ? WlrLayer.Overlay : LayerShell.fromEnv("DMS_MODAL_LAYER", WlrLayer.Top, {
"allow": ["top", "overlay"],
"invalidLayer": WlrLayer.Top,
"label": "modals",
"error": true
})
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
@@ -536,11 +545,13 @@ Item {
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible)
return;
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
}
}
@@ -90,7 +90,6 @@ Item {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
opened();
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
}
@@ -252,12 +251,22 @@ Item {
}
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: root.useOverlayLayer ? WlrLayer.Overlay : LayerShell.fromEnv("DMS_MODAL_LAYER", WlrLayer.Top, {
"allow": ["top", "overlay"],
"invalidLayer": WlrLayer.Top,
"label": "modals",
"error": true
})
WlrLayershell.layer: {
if (root.useOverlayLayer)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
@@ -287,11 +296,13 @@ Item {
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible)
return;
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
}
}
@@ -57,11 +57,7 @@ Rectangle {
return;
if (response.error)
return;
if (!response.result) {
ClipboardService.refresh();
return;
}
const result = response.result;
const result = response.result ?? {};
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
const data = (result.data ?? "").toString();
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
@@ -1721,15 +1721,11 @@ Item {
return "";
var idx = text.toLowerCase().indexOf(lowerQuery);
if (idx === -1)
return _escapeRichText(text);
return text;
var before = text.substring(0, idx);
var match = text.substring(idx, idx + queryLen);
var after = text.substring(idx + queryLen);
return '<span style="color:' + baseColor + '">' + _escapeRichText(before) + '</span><span style="color:' + highlightColor + '; font-weight:600">' + _escapeRichText(match) + '</span><span style="color:' + baseColor + '">' + _escapeRichText(after) + '</span>';
}
function _escapeRichText(text) {
return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
return '<span style="color:' + baseColor + '">' + before + '</span><span style="color:' + highlightColor + '; font-weight:600">' + match + '</span><span style="color:' + baseColor + '">' + after + '</span>';
}
function getCurrentSectionViewMode() {
@@ -42,12 +42,20 @@ Item {
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool usesOverlayLayer: SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"],
"invalidLayer": WlrLayer.Top,
"label": "modals",
"error": true
})
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
@@ -681,7 +689,7 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
@@ -32,12 +32,20 @@ Item {
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"],
"invalidLayer": WlrLayer.Top,
"label": "modals",
"error": true
})
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property int _openDuration: 50
readonly property int _closeDuration: 40
@@ -337,7 +345,7 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
@@ -81,12 +81,20 @@ Item {
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"],
"invalidLayer": WlrLayer.Top,
"label": "modals",
"error": true
})
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property real cornerRadius: Theme.cornerRadius
readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
@@ -373,7 +381,7 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
@@ -446,7 +446,7 @@ Item {
WlrLayershell.namespace: "dms:launcher-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
anchors {
top: true
@@ -25,7 +25,7 @@ Popup {
dangerous: true
},
{
text: I18n.tr("Copy path"),
text: I18n.tr("Copy Path"),
icon: "content_copy",
action: copyPath,
enabled: filePath.length > 0
-277
View File
@@ -1,277 +0,0 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
import Quickshell.Services.UPower
DankModal {
id: root
layerNamespace: "dms:power-profiles"
keepPopoutsOpen: true
property int selectedIndex: 0
property var profileModel: PowerProfileWatcher.availableProfiles
function openCentered() {
open();
}
function hideDialog() {
close();
}
shouldBeVisible: false
modalWidth: 440
modalHeight: 290
enableShadow: true
onBackgroundClicked: hideDialog()
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
return;
if (typeof PowerProfiles !== "undefined") {
const current = PowerProfiles.profile;
const idx = profileModel.indexOf(current);
if (idx !== -1) {
selectedIndex = idx;
}
}
}
onShouldHaveFocusChanged: {
if (!shouldHaveFocus)
return;
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
modalFocusScope.Keys.onPressed: event => {
if (event.isAutoRepeat) {
event.accepted = true;
return;
}
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + profileModel.length) % profileModel.length;
event.accepted = true;
break;
case Qt.Key_Right:
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % profileModel.length;
event.accepted = true;
break;
case Qt.Key_Space:
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedIndex >= 0 && selectedIndex < profileModel.length) {
setProfile(profileModel[selectedIndex]);
}
event.accepted = true;
break;
case Qt.Key_1:
if (profileModel.length > 0) {
setProfile(profileModel[0]);
}
event.accepted = true;
break;
case Qt.Key_2:
if (profileModel.length > 1) {
setProfile(profileModel[1]);
}
event.accepted = true;
break;
case Qt.Key_3:
if (profileModel.length > 2) {
setProfile(profileModel[2]);
}
event.accepted = true;
break;
case Qt.Key_Escape:
hideDialog();
event.accepted = true;
break;
}
}
function setProfile(profile) {
if (PowerProfileWatcher.applyProfile(profile)) {
hideDialog();
return;
}
if (!PowerProfileWatcher.available)
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
else
ToastService.showError(I18n.tr("Failed to set power profile"));
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Power Mode")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Choose a power profile")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hideDialog()
}
}
Row {
id: buttonsRow
width: parent.width
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: root.profileModel
Rectangle {
id: profileButton
required property int index
required property int modelData
readonly property bool isSelected: root.selectedIndex === index
readonly property bool isActive: (typeof PowerProfiles !== "undefined") && PowerProfiles.profile === modelData
width: (parent.width - Theme.spacingM * (root.profileModel.length - 1)) / root.profileModel.length
height: 120
radius: Theme.cornerRadius
color: {
if (isActive)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (mouseArea.containsMouse)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
}
border.color: isActive ? Theme.primary : (isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) : "transparent")
border.width: (isActive || isSelected) ? 2 : 0
// Shortcut Key Badge on Top-Right Corner
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingS
width: 20
height: 20
radius: 4
color: isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
border.color: isActive ? Theme.primary : "transparent"
border.width: isActive ? 1 : 0
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: isActive ? Theme.primary : Theme.surfaceTextMedium
anchors.centerIn: parent
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Theme.getPowerProfileIcon(modelData)
size: Theme.iconSize + 16
color: isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: Theme.getPowerProfileLabel(modelData)
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.selectedIndex = index;
}
onClicked: {
root.setProfile(modelData);
}
}
}
}
}
// Selected power profile description
StyledText {
text: (root.selectedIndex >= 0 && root.selectedIndex < root.profileModel.length) ? Theme.getPowerProfileDescription(root.profileModel[root.selectedIndex]) : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
width: parent.width - Theme.spacingL * 2
}
// Keyboard Shortcut Guide Footer
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS
opacity: 0.5
DankIcon {
name: "keyboard"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Use keys 1-3 or arrows, Enter/Space to select")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceText
}
}
}
}
}
}
@@ -570,22 +570,5 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: autoStartLoader
anchors.fill: parent
active: root.currentIndex === 36
visible: active
focus: active
sourceComponent: AutoStartTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
@@ -245,13 +245,6 @@ Rectangle {
"icon": "app_registration",
"tabIndex": 19,
"hyprlandNiriOnly": true
},
{
"id": "autostart",
"text": I18n.tr("Autostart Apps"),
"icon": "line_start",
"tabIndex": 36,
"autostartOnly": true
}
]
},
@@ -376,8 +369,6 @@ Rectangle {
return false;
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
return false;
if (item.autostartOnly && !DesktopService.autostartAvailable)
return false;
return true;
}
+1 -1
View File
@@ -250,7 +250,7 @@ DankModal {
DankButton {
visible: SessionsService.otherSessions().length === 0 && !root.lockOnSwitch
text: I18n.tr("Log Out")
text: I18n.tr("Log out")
iconName: "logout"
backgroundColor: Theme.primary
textColor: Theme.primaryText
+9 -441
View File
@@ -20,10 +20,6 @@ FloatingWindow {
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
readonly property int sectionSpacing: Theme.spacingL
ListModel {
id: extraMatchModel
}
objectName: "windowRuleModal"
title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("Create Window Rule")
minimumSize: Qt.size(500, 600)
@@ -35,18 +31,6 @@ FloatingWindow {
nameInput.text = "";
appIdInput.text = "";
titleInput.text = "";
extraMatchModel.clear();
condFloating.triState = 0;
condActive.triState = 0;
condFocused.triState = 0;
condActiveInColumn.triState = 0;
condCastTarget.triState = 0;
condUrgent.triState = 0;
condAtStartup.triState = 0;
condXwayland.triState = 0;
condFullscreen.triState = 0;
condPinned.triState = 0;
condInitialised.triState = 0;
opacityEnabled.checked = false;
opacitySlider.value = 100;
floatingToggle.checked = false;
@@ -68,12 +52,6 @@ FloatingWindow {
clipToGeometryToggle.checked = false;
tiledStateToggle.checked = false;
drawBorderBgToggle.checked = false;
blurCond.triState = 0;
xrayCond.triState = 0;
noiseEnabled.checked = false;
noiseSlider.value = 5;
saturationEnabled.checked = false;
saturationSlider.value = 100;
minWidthInput.text = "";
maxWidthInput.text = "";
minHeightInput.text = "";
@@ -106,39 +84,18 @@ FloatingWindow {
Qt.callLater(() => nameInput.forceActiveFocus());
}
function triFromBool(v) {
if (v === true)
return 1;
if (v === false)
return 2;
return 0;
}
function showEdit(rule) {
if (!rule) {
show();
return;
}
editingRule = rule;
resetForm();
function populateForm(rule) {
nameInput.text = rule.name || "";
const matchList = (rule.matches && rule.matches.length > 0) ? rule.matches : [rule.matchCriteria || {}];
const match = matchList[0] || {};
const match = rule.matchCriteria || {};
appIdInput.text = match.appId || "";
titleInput.text = match.title || "";
extraMatchModel.clear();
for (let i = 1; i < matchList.length; i++) {
extraMatchModel.append({
"rowAppId": matchList[i].appId || "",
"rowTitle": matchList[i].title || ""
});
}
condFloating.triState = triFromBool(match.isFloating);
condActive.triState = triFromBool(match.isActive);
condFocused.triState = triFromBool(match.isFocused);
condActiveInColumn.triState = triFromBool(match.isActiveInColumn);
condCastTarget.triState = triFromBool(match.isWindowCastTarget);
condUrgent.triState = triFromBool(match.isUrgent);
condAtStartup.triState = triFromBool(match.atStartup);
condXwayland.triState = triFromBool(match.xwayland);
condFullscreen.triState = triFromBool(match.fullscreen);
condPinned.triState = triFromBool(match.pinned);
condInitialised.triState = triFromBool(match.initialised);
const actions = rule.actions || {};
const hasOpacity = actions.opacity !== undefined && actions.opacity !== null;
@@ -174,15 +131,6 @@ FloatingWindow {
drawBorderBgToggle.checked = actions.drawBorderWithBackground || false;
xrayCond.triState = triFromBool(actions.backgroundXray);
blurCond.triState = triFromBool(actions.backgroundBlur);
const hasNoise = actions.backgroundNoise !== undefined && actions.backgroundNoise !== null;
noiseEnabled.checked = hasNoise;
noiseSlider.value = hasNoise ? Math.round(actions.backgroundNoise * 100) : 5;
const hasSaturation = actions.backgroundSaturation !== undefined && actions.backgroundSaturation !== null;
saturationEnabled.checked = hasSaturation;
saturationSlider.value = hasSaturation ? Math.round(actions.backgroundSaturation * 100) : 100;
minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : "";
maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : "";
minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : "";
@@ -202,28 +150,7 @@ FloatingWindow {
moveInput.text = actions.move || "";
monitorInput.text = actions.monitor || "";
hyprWorkspaceInput.text = actions.workspace || "";
}
function showEdit(rule) {
if (!rule) {
show();
return;
}
editingRule = rule;
resetForm();
populateForm(rule);
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
}
function showCopy(rule) {
if (!rule) {
show();
return;
}
editingRule = null;
resetForm();
populateForm(rule);
visible = true;
Qt.callLater(() => nameInput.forceActiveFocus());
}
@@ -234,13 +161,6 @@ FloatingWindow {
targetWindow = null;
}
function applyCond(obj, key, triState) {
if (triState === 1)
obj[key] = true;
else if (triState === 2)
obj[key] = false;
}
function submitAndClose() {
const matchCriteria = {};
if (appIdInput.text.trim())
@@ -248,38 +168,6 @@ FloatingWindow {
if (titleInput.text.trim())
matchCriteria.title = titleInput.text.trim();
applyCond(matchCriteria, "isFloating", condFloating.triState);
if (isNiri) {
applyCond(matchCriteria, "isActive", condActive.triState);
applyCond(matchCriteria, "isFocused", condFocused.triState);
applyCond(matchCriteria, "isActiveInColumn", condActiveInColumn.triState);
applyCond(matchCriteria, "isWindowCastTarget", condCastTarget.triState);
applyCond(matchCriteria, "isUrgent", condUrgent.triState);
applyCond(matchCriteria, "atStartup", condAtStartup.triState);
}
if (isHyprland) {
applyCond(matchCriteria, "xwayland", condXwayland.triState);
applyCond(matchCriteria, "fullscreen", condFullscreen.triState);
applyCond(matchCriteria, "pinned", condPinned.triState);
applyCond(matchCriteria, "initialised", condInitialised.triState);
}
const matches = [];
if (Object.keys(matchCriteria).length > 0)
matches.push(matchCriteria);
if (isNiri) {
for (let i = 0; i < extraMatchModel.count; i++) {
const row = extraMatchModel.get(i);
const m = {};
if ((row.rowAppId || "").trim())
m.appId = row.rowAppId.trim();
if ((row.rowTitle || "").trim())
m.title = row.rowTitle.trim();
if (Object.keys(m).length > 0)
matches.push(m);
}
}
const actions = {};
if (opacityEnabled.checked)
@@ -318,14 +206,6 @@ FloatingWindow {
actions.tiledState = true;
if (drawBorderBgToggle.checked && isNiri)
actions.drawBorderWithBackground = true;
if (isNiri) {
applyCond(actions, "backgroundBlur", blurCond.triState);
applyCond(actions, "backgroundXray", xrayCond.triState);
}
if (noiseEnabled.checked && isNiri)
actions.backgroundNoise = noiseSlider.value / 100;
if (saturationEnabled.checked && isNiri)
actions.backgroundSaturation = saturationSlider.value / 100;
const minW = parseInt(minWidthInput.text);
const maxW = parseInt(maxWidthInput.text);
@@ -380,8 +260,6 @@ FloatingWindow {
actions: actions,
enabled: true
};
if (isNiri && extraMatchModel.count > 0)
ruleData.matches = matches;
submitting = true;
@@ -491,61 +369,6 @@ FloatingWindow {
border.width: hasFocus ? 2 : 1
}
// Tri-state toggle: 0 = unset (Inherit/Any), 1 = true, 2 = false
component MatchCond: Rectangle {
id: mc
property string label: ""
property int triState: 0
property string unsetLabel: I18n.tr("Any")
property bool readOnly: false
readonly property var stateText: [mc.unsetLabel, "true", "false"]
readonly property var stateColor: [Theme.surfaceVariantText, Theme.primary, Theme.error]
width: condRow.implicitWidth + Theme.spacingM * 2
height: root.inputFieldHeight
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.width: 1
border.color: mc.triState === 0 ? Theme.outlineStrong : mc.stateColor[mc.triState]
opacity: mc.readOnly ? 0.4 : 1
Row {
id: condRow
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: mc.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: stateBadge.implicitWidth + Theme.spacingS * 2
height: 18
radius: 9
color: Theme.withAlpha(mc.stateColor[mc.triState], 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: stateBadge
anchors.centerIn: parent
text: mc.stateText[mc.triState]
font.pixelSize: Theme.fontSizeSmall - 2
color: mc.stateColor[mc.triState]
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
enabled: root.visible && !mc.readOnly
onClicked: mc.triState = (mc.triState + 1) % 3
}
}
FocusScope {
anchors.fill: parent
focus: true
@@ -691,176 +514,6 @@ FloatingWindow {
}
}
StyledText {
width: parent.width
visible: root.isNiri
text: I18n.tr("The rule applies to any window matching one of these.")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Repeater {
model: extraMatchModel
delegate: Row {
width: parent.width
spacing: Theme.spacingS
InputField {
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
hasFocus: extraAppId.activeFocus
DankTextField {
id: extraAppId
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: root.isNiri ? I18n.tr("App ID regex") : I18n.tr("Class regex")
backgroundColor: "transparent"
enabled: root.visible
text: rowAppId
onTextEdited: extraMatchModel.setProperty(index, "rowAppId", text)
}
}
InputField {
width: (parent.width - removeMatchBtn.width - Theme.spacingS * 2) / 2
hasFocus: extraTitle.activeFocus
DankTextField {
id: extraTitle
anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText
placeholderText: I18n.tr("Title regex (optional)")
backgroundColor: "transparent"
enabled: root.visible
text: rowTitle
onTextEdited: extraMatchModel.setProperty(index, "rowTitle", text)
}
}
DankActionButton {
id: removeMatchBtn
width: root.inputFieldHeight
height: root.inputFieldHeight
circular: false
iconName: "close"
iconSize: 16
iconColor: Theme.surfaceVariantText
tooltipText: I18n.tr("Remove match")
tooltipSide: "left"
onClicked: extraMatchModel.remove(index)
}
}
}
Item {
width: parent.width
height: root.inputFieldHeight
visible: root.isNiri
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "add"
size: 18
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Add match")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: extraMatchModel.append({
"rowAppId": "",
"rowTitle": ""
})
}
}
SectionHeader {
title: I18n.tr("Match Conditions")
}
StyledText {
width: parent.width
text: I18n.tr("Optional state-based conditions applied to the first match.")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Flow {
width: parent.width
spacing: Theme.spacingS
MatchCond {
id: condFloating
label: I18n.tr("Floating")
}
MatchCond {
id: condActive
label: I18n.tr("Active")
visible: isNiri
}
MatchCond {
id: condFocused
label: I18n.tr("Focused")
visible: isNiri
}
MatchCond {
id: condActiveInColumn
label: I18n.tr("Active in Column")
visible: isNiri
}
MatchCond {
id: condCastTarget
label: I18n.tr("Cast Target")
visible: isNiri
}
MatchCond {
id: condUrgent
label: I18n.tr("Urgent")
visible: isNiri
}
MatchCond {
id: condAtStartup
label: I18n.tr("At Startup")
visible: isNiri
}
MatchCond {
id: condXwayland
label: I18n.tr("XWayland")
visible: isHyprland
}
MatchCond {
id: condFullscreen
label: I18n.tr("Fullscreen")
visible: isHyprland
}
MatchCond {
id: condPinned
label: I18n.tr("Pinned")
visible: isHyprland
}
MatchCond {
id: condInitialised
label: I18n.tr("Initialised")
visible: isHyprland
}
}
SectionHeader {
title: I18n.tr("Window Opening")
}
@@ -1029,7 +682,6 @@ FloatingWindow {
DankSlider {
id: opacitySlider
wheelEnabled: false
width: parent.width - 100
minimum: 10
maximum: 100
@@ -1058,7 +710,7 @@ FloatingWindow {
}
CheckboxRow {
id: drawBorderBgToggle
label: I18n.tr("Border with Background")
label: I18n.tr("Border with BG")
}
}
@@ -1125,7 +777,6 @@ FloatingWindow {
DankSlider {
id: scrollFactorSlider
wheelEnabled: false
width: parent.width - 120
minimum: 10
maximum: 200
@@ -1147,7 +798,6 @@ FloatingWindow {
DankSlider {
id: cornerRadiusSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 24
@@ -1157,88 +807,6 @@ FloatingWindow {
}
}
SectionHeader {
title: I18n.tr("Background Effect")
visible: isNiri
}
StyledText {
width: parent.width
visible: isNiri
text: I18n.tr("Xray blurs only the wallpaper (efficient) and is the default when Blur is on. Set Xray to Off for regular full blur of everything beneath the window (more expensive).")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Flow {
width: parent.width
spacing: Theme.spacingS
visible: isNiri
MatchCond {
id: blurCond
label: I18n.tr("Blur")
unsetLabel: I18n.tr("Inherit")
onTriStateChanged: {
if (triState === 2)
xrayCond.triState = 0;
}
}
MatchCond {
id: xrayCond
label: I18n.tr("X-Ray")
unsetLabel: I18n.tr("Inherit")
readOnly: blurCond.triState === 2
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri
CheckboxRow {
id: noiseEnabled
label: I18n.tr("Noise")
anchors.verticalCenter: parent.verticalCenter
}
DankSlider {
id: noiseSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 100
value: 5
enabled: noiseEnabled.checked
opacity: enabled ? 1 : 0.4
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: isNiri
CheckboxRow {
id: saturationEnabled
label: I18n.tr("Saturation")
anchors.verticalCenter: parent.verticalCenter
}
DankSlider {
id: saturationSlider
wheelEnabled: false
width: parent.width - 130
minimum: 0
maximum: 200
value: 100
enabled: saturationEnabled.checked
opacity: enabled ? 1 : 0.4
}
}
SectionHeader {
title: I18n.tr("Size Constraints")
}
@@ -126,14 +126,6 @@ Variants {
}
}
Connections {
target: SettingsData
function onWallpaperFillModeChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Timer {
id: renderSettleTimer
interval: 1000
@@ -15,6 +15,25 @@ Item {
property var pluginDetailInstance: null
property var widgetModel: null
property var collapseCallback: null
property real maxAvailableHeight: 9999
function getDetailHeight(section) {
switch (true) {
case section === "wifi":
case section === "bluetooth":
case section === "builtin_vpn":
case section === "builtin_tailscale":
return Math.min(350, maxAvailableHeight);
case section.startsWith("brightnessSlider_"):
return Math.min(400, maxAvailableHeight);
case section.startsWith("plugin_"):
if (pluginDetailInstance?.ccDetailHeight)
return Math.min(pluginDetailInstance.ccDetailHeight, maxAvailableHeight);
return Math.min(250, maxAvailableHeight);
default:
return Math.min(250, maxAvailableHeight);
}
}
Loader {
id: pluginDetailLoader
@@ -1,38 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var widgetData: null
signal showMountPathChanged(bool show)
width: 260
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.16)
border.width: 1
MouseArea {
anchors.fill: parent
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
DankToggle {
width: parent.width
text: I18n.tr("Show mount path", "toggle in control center disk usage widget to turn mount path display on or off")
checked: root.widgetData?.showMountPath !== false
onToggled: newChecked => {
root.showMountPathChanged(newChecked);
}
}
}
}
@@ -3,7 +3,6 @@ import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Components
import "../utils/detailHeight.js" as DetailHeightUtils
import "../utils/layout.js" as LayoutUtils
Column {
@@ -26,7 +25,6 @@ Column {
signal moveWidget(int fromIndex, int toIndex)
signal toggleWidgetSize(int index)
signal collapseRequested
signal configRequested(int index, var widgetData, var anchor)
function requestCollapse() {
collapseRequested();
@@ -39,7 +37,6 @@ Column {
property real currentRowWidth: 0
property int expandedRowIndex: -1
property var colorPickerModal: null
property var activePluginDetailInstance: null
readonly property real _maxDetailHeight: {
const rows = layoutResult.rows;
@@ -56,8 +53,6 @@ Column {
}
readonly property real targetImplicitHeight: {
if (editMode)
return editModeGrid.implicitHeight;
const rows = layoutResult.rows;
let totalHeight = 0;
for (let i = 0; i < rows.length; i++) {
@@ -75,7 +70,15 @@ Column {
}
function detailHeightForSection(section) {
return DetailHeightUtils.detailHeightForSection(section, _maxDetailHeight, activePluginDetailInstance);
if (!section)
return 0;
if (section === "wifi" || section === "bluetooth" || section === "builtin_vpn")
return Math.min(350, _maxDetailHeight);
if (section.startsWith("brightnessSlider_"))
return Math.min(400, _maxDetailHeight);
if (section.startsWith("plugin_"))
return Math.min(250, _maxDetailHeight);
return Math.min(250, _maxDetailHeight);
}
function calculateRowsAndWidgets() {
@@ -102,40 +105,8 @@ Column {
item.z = 1000;
}
function componentForWidget(widgetData) {
const id = widgetData.id || "";
const widgetWidth = widgetData.width || 50;
if (id.startsWith("builtin_"))
return builtinPluginWidgetComponent;
if (id.startsWith("plugin_"))
return pluginWidgetComponent;
switch (id) {
case "wifi":
case "bluetooth":
case "audioOutput":
case "audioInput":
return compoundPillComponent;
case "volumeSlider":
return audioSliderComponent;
case "brightnessSlider":
return brightnessSliderComponent;
case "inputVolumeSlider":
return inputAudioSliderComponent;
case "battery":
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent;
case "diskUsage":
return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent;
case "colorPicker":
return colorPickerPillComponent;
case "doNotDisturb":
return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent;
default:
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent;
}
}
Repeater {
model: root.editMode ? [] : root.layoutResult.rows
model: root.layoutResult.rows
Column {
width: root.width
@@ -202,12 +173,36 @@ Column {
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider";
}
widgetComponent: root.componentForWidget(modelData)
widgetComponent: {
const id = modelData.id || "";
if (id.startsWith("builtin_")) {
return builtinPluginWidgetComponent;
} else if (id.startsWith("plugin_")) {
return pluginWidgetComponent;
} else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
return compoundPillComponent;
} else if (id === "volumeSlider") {
return audioSliderComponent;
} else if (id === "brightnessSlider") {
return brightnessSliderComponent;
} else if (id === "inputVolumeSlider") {
return inputAudioSliderComponent;
} else if (id === "battery") {
return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent;
} else if (id === "diskUsage") {
return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent;
} else if (id === "colorPicker") {
return colorPickerPillComponent;
} else if (id === "doNotDisturb") {
return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent;
} else {
return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent;
}
}
onWidgetMoved: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex)
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor)
}
}
}
@@ -215,6 +210,7 @@ Column {
DetailHost {
id: detailHost
width: parent.width
maxAvailableHeight: root._maxDetailHeight
height: active ? (root.detailHeightForSection(root.expandedSection) + Theme.spacingS) : 0
clip: true
property string retainedSection: ""
@@ -251,19 +247,7 @@ Column {
retainedWidgetData = root.expandedWidgetData;
}
function syncActivePluginDetail() {
if (active) {
root.activePluginDetailInstance = pluginDetailInstance;
} else if (root.activePluginDetailInstance === pluginDetailInstance) {
root.activePluginDetailInstance = null;
}
}
onActiveChanged: {
retainActiveDetail();
syncActivePluginDetail();
}
onPluginDetailInstanceChanged: syncActivePluginDetail()
onActiveChanged: retainActiveDetail()
onHeightChanged: {
if (!active && height <= 0.5) {
retainedSection = "";
@@ -293,18 +277,6 @@ Column {
}
}
EditModeGrid {
id: editModeGrid
width: root.width
visible: root.editMode
active: root.editMode
model: root.model
componentProvider: root
onRemoveWidget: index => root.removeWidget(index)
onToggleWidgetSize: index => root.toggleWidgetSize(index)
onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor)
}
Component {
id: errorPillComponent
ErrorPill {
@@ -897,7 +869,6 @@ Column {
mountPath: widgetData.mountPath || "/"
instanceId: widgetData.instanceId || ""
showMountPath: widgetData.showMountPath !== undefined ? widgetData.showMountPath : true
onExpandClicked: {
if (!root.editMode) {
@@ -917,7 +888,6 @@ Column {
mountPath: widgetData.mountPath || "/"
instanceId: widgetData.instanceId || ""
showMountPath: widgetData.showMountPath !== undefined ? widgetData.showMountPath : true
onClicked: {
if (!root.editMode) {
@@ -21,7 +21,6 @@ Item {
signal widgetMoved(int fromIndex, int toIndex)
signal removeWidget(int index)
signal toggleWidgetSize(int index)
signal configRequested(int index, var widgetData, var anchor)
width: {
const widgetWidth = widgetData?.width || 50;
@@ -237,7 +236,6 @@ Item {
}
Rectangle {
id: removeButton
width: 16
height: 16
radius: 8
@@ -280,34 +278,6 @@ Item {
}
}
readonly property bool hasConfigMenu: widgetData?.id === "diskUsage"
Rectangle {
id: configButton
width: 16
height: 16
radius: 8
color: Theme.primary
anchors.top: removeButton.top
anchors.right: removeButton.left
anchors.rightMargin: 4
visible: editMode && root.hasConfigMenu
z: 10
DankIcon {
anchors.centerIn: parent
name: "settings"
size: 12
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.configRequested(root.widgetIndex, root.widgetData, configButton)
}
}
Rectangle {
id: dragHandle
width: 16
@@ -50,7 +50,7 @@ Row {
WlrLayershell.namespace: "dms:control-center-widget-library"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
anchors {
top: true
@@ -1,110 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Components
import "../utils/layout.js" as LayoutUtils
Item {
id: root
property var model: null
property var componentProvider: null
property bool active: true
signal removeWidget(int index)
signal toggleWidgetSize(int index)
signal configRequested(int index, var widgetData, var anchor)
property var sourceWidgets: SettingsData.controlCenterWidgets || []
property var visualOrder: []
property int draggingSourceIndex: -1
property var dragStartOrder: []
readonly property real rowSpacing: Theme.spacingL
readonly property real sliderCellHeight: 48
readonly property real normalCellHeight: 60
readonly property var slotLayout: LayoutUtils.computeSlots(sourceWidgets, visualOrder, width, Theme.spacingS, rowSpacing, sliderCellHeight, normalCellHeight)
implicitHeight: slotLayout.totalHeight
function rebuildOrder() {
const n = (sourceWidgets || []).length;
const arr = [];
for (var i = 0; i < n; i++)
arr.push(i);
visualOrder = arr;
}
onSourceWidgetsChanged: rebuildOrder()
Component.onCompleted: rebuildOrder()
function beginDrag(sourceIndex) {
draggingSourceIndex = sourceIndex;
dragStartOrder = visualOrder.slice();
}
function sameOrder(a, b) {
if (a.length !== b.length)
return false;
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false;
}
return true;
}
function updateDragTarget(centerX, centerY) {
if (draggingSourceIndex < 0)
return;
const p = LayoutUtils.slotContainingPoint(slotLayout.slots, visualOrder, centerX, centerY);
if (p < 0)
return;
const arr = visualOrder.slice();
const d = arr.indexOf(draggingSourceIndex);
if (d < 0 || d === p)
return;
arr.splice(d, 1);
arr.splice(p, 0, draggingSourceIndex);
visualOrder = arr;
}
function endDrag() {
if (draggingSourceIndex < 0)
return;
draggingSourceIndex = -1;
if (!sameOrder(visualOrder, dragStartOrder))
commit();
}
function commit() {
const widgets = sourceWidgets || [];
const arr = visualOrder.map(i => widgets[i]);
if (root.model)
root.model.reorderWidgets(arr);
}
Repeater {
model: root.active ? root.sourceWidgets : []
EditModeWidgetDelegate {
required property int index
required property var modelData
grid: root
sourceIndex: index
widgetData: modelData
isSlider: LayoutUtils.isSliderWidget(modelData.id || "")
widgetComponent: root.componentProvider ? root.componentProvider.componentForWidget(modelData) : null
slotX: root.slotLayout.slots[index] ? root.slotLayout.slots[index].x : 0
slotY: root.slotLayout.slots[index] ? root.slotLayout.slots[index].y : 0
cellW: root.slotLayout.slots[index] ? root.slotLayout.slots[index].w : root.width
cellH: root.slotLayout.slots[index] ? root.slotLayout.slots[index].h : root.normalCellHeight
onRemoveWidget: idx => root.removeWidget(idx)
onToggleWidgetSize: idx => root.toggleWidgetSize(idx)
onConfigRequested: (idx, data, anchor) => root.configRequested(idx, data, anchor)
}
}
}
@@ -1,242 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var grid: null
property int sourceIndex: -1
property var widgetData: null
property Component widgetComponent: null
property bool isSlider: false
property real slotX: 0
property real slotY: 0
property real cellW: 100
property real cellH: 60
property bool dragging: !!grid && grid.draggingSourceIndex === sourceIndex
signal removeWidget(int index)
signal toggleWidgetSize(int index)
signal configRequested(int index, var widgetData, var anchor)
width: cellW
height: cellH
z: dragging ? 10000 : 1
Binding {
target: root
property: "x"
value: root.slotX
when: !root.dragging
restoreMode: Binding.RestoreNone
}
Binding {
target: root
property: "y"
value: root.slotY
when: !root.dragging
restoreMode: Binding.RestoreNone
}
onXChanged: {
if (dragging && grid)
grid.updateDragTarget(x + width / 2, y + height / 2);
}
onYChanged: {
if (dragging && grid)
grid.updateDragTarget(x + width / 2, y + height / 2);
}
Behavior on x {
enabled: !root.dragging
NumberAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Behavior on y {
enabled: !root.dragging
NumberAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
Rectangle {
id: dragIndicator
anchors.fill: parent
color: "transparent"
border.color: Theme.primary
border.width: root.dragging ? 2 : 0
radius: Theme.cornerRadius
opacity: root.dragging ? 0.8 : 1.0
z: root.dragging ? 10000 : 1
Behavior on border.width {
NumberAnimation {
duration: 150
}
}
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
}
Loader {
id: widgetLoader
anchors.fill: parent
sourceComponent: root.widgetComponent
property var widgetData: root.widgetData
property int widgetIndex: root.sourceIndex
property int globalWidgetIndex: root.sourceIndex
property int widgetWidth: root.widgetData?.width || 50
MouseArea {
id: editModeBlocker
anchors.fill: parent
enabled: true
acceptedButtons: Qt.AllButtons
onPressed: function (mouse) {
mouse.accepted = true;
}
onWheel: function (wheel) {
wheel.accepted = true;
}
z: 100
}
}
MouseArea {
id: dragArea
anchors.fill: parent
cursorShape: Qt.OpenHandCursor
drag.target: root
drag.axis: Drag.XAndYAxis
drag.smoothed: false
onPressed: function (mouse) {
cursorShape = Qt.ClosedHandCursor;
if (root.grid)
root.grid.beginDrag(root.sourceIndex);
}
onReleased: function (mouse) {
cursorShape = Qt.OpenHandCursor;
if (root.grid)
root.grid.endDrag();
}
}
Rectangle {
id: removeButton
width: 16
height: 16
radius: 8
color: Theme.error
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: -4
z: 10
DankIcon {
anchors.centerIn: parent
name: "close"
size: 12
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.removeWidget(root.sourceIndex)
}
}
SizeControls {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: -6
z: 10
currentSize: root.widgetData?.width || 50
isSlider: root.isSlider
widgetIndex: root.sourceIndex
onSizeChanged: newSize => {
var widgets = SettingsData.controlCenterWidgets.slice();
if (root.sourceIndex >= 0 && root.sourceIndex < widgets.length) {
widgets[root.sourceIndex].width = newSize;
SettingsData.set("controlCenterWidgets", widgets);
}
}
}
readonly property bool hasConfigMenu: widgetData?.id === "diskUsage"
Rectangle {
id: configButton
width: 16
height: 16
radius: 8
color: Theme.primary
anchors.top: removeButton.top
anchors.right: removeButton.left
anchors.rightMargin: 4
visible: root.hasConfigMenu
z: 10
DankIcon {
anchors.centerIn: parent
name: "settings"
size: 12
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.configRequested(root.sourceIndex, root.widgetData, configButton)
}
}
Rectangle {
id: dragHandle
width: 16
height: 12
radius: 2
color: Theme.primary
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
z: 15
opacity: root.dragging ? 1.0 : 0.7
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 10
color: Theme.primaryText
}
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
radius: Theme.cornerRadius
border.color: "transparent"
border.width: 0
z: -1
}
}
@@ -60,7 +60,7 @@ Rectangle {
}
Typography {
text: DgopService.uptime ? I18n.tr("up") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown")
text: DgopService.uptime || I18n.tr("Unknown")
style: Typography.Style.Caption
color: Theme.surfaceVariantText
}
@@ -1,77 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property int widgetIndex: -1
property real anchorX: 0
property real anchorY: 0
property real anchorWidth: 0
property real anchorHeight: 0
readonly property var widgetData: {
if (widgetIndex < 0)
return null;
const widgets = SettingsData.controlCenterWidgets || [];
return widgets[widgetIndex] || null;
}
visible: widgetIndex >= 0
z: 10000
function open(index, data, anchorItem) {
const pos = anchorItem.mapToItem(root, 0, 0);
anchorX = pos.x;
anchorY = pos.y;
anchorWidth = anchorItem.width;
anchorHeight = anchorItem.height;
widgetIndex = index;
}
function close() {
widgetIndex = -1;
}
function persistShowMountPath(show) {
const widgets = (SettingsData.controlCenterWidgets || []).slice();
if (root.widgetIndex < 0 || root.widgetIndex >= widgets.length)
return;
const updated = Object.assign({}, widgets[root.widgetIndex]);
updated.showMountPath = show;
widgets[root.widgetIndex] = updated;
SettingsData.set("controlCenterWidgets", widgets);
}
MouseArea {
anchors.fill: parent
enabled: root.visible
onClicked: root.close()
}
DiskUsageWidgetConfigMenu {
id: diskMenu
visible: root.visible && root.widgetData?.id === "diskUsage"
widgetData: root.widgetData
x: {
let nx = root.anchorX + root.anchorWidth - width;
const maxX = root.width - width - Theme.spacingS;
const minX = Theme.spacingS;
if (nx < minX)
nx = minX;
if (nx > maxX)
nx = maxX;
return nx;
}
y: {
let ny = root.anchorY - height - Theme.spacingS;
if (ny < Theme.spacingS)
ny = root.anchorY + root.anchorHeight + Theme.spacingS;
return ny;
}
onShowMountPathChanged: show => root.persistShowMountPath(show)
}
}
@@ -273,7 +273,6 @@ DankPopout {
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
onCollapseRequested: root.collapseAll()
onConfigRequested: (idx, data, anchor) => widgetConfigOverlay.open(idx, data, anchor)
}
EditControls {
@@ -304,11 +303,6 @@ DankPopout {
anchors.fill: parent
z: 10000
}
WidgetConfigOverlay {
id: widgetConfigOverlay
anchors.fill: parent
}
}
}
@@ -24,13 +24,14 @@ Rectangle {
}
function setProfile(profile) {
if (PowerProfileWatcher.applyProfile(profile))
return;
if (!PowerProfileWatcher.available)
if (typeof PowerProfiles === "undefined") {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
else
return;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) {
ToastService.showError(I18n.tr("Failed to set power profile"));
}
}
Column {
@@ -192,7 +193,7 @@ Rectangle {
}
DankButtonGroup {
property var profileModel: PowerProfileWatcher.availableProfiles
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
@@ -245,7 +245,7 @@ QtObject {
{
"id": "builtin_vpn",
"text": I18n.tr("VPN"),
"description": I18n.tr("VPN Connections"),
"description": I18n.tr("VPN connections"),
"icon": "vpn_key",
"type": "builtin_plugin",
"enabled": DMSNetworkService.available,
@@ -352,10 +352,6 @@ QtObject {
WidgetUtils.moveWidget(fromIndex, toIndex);
}
function reorderWidgets(newOrder) {
WidgetUtils.reorderWidgets(newOrder);
}
function resetToDefault() {
WidgetUtils.resetToDefault();
}
@@ -25,7 +25,7 @@ CompoundPill {
return `${BatteryService.batteryLevel}% ` + I18n.tr("Charging");
}
if (BatteryService.isPluggedIn) {
return `${BatteryService.batteryLevel}% ` + I18n.tr("Plugged In");
return `${BatteryService.batteryLevel}% ` + I18n.tr("Plugged in");
}
return `${BatteryService.batteryLevel}%`;
}
@@ -8,7 +8,6 @@ CompoundPill {
property string mountPath: "/"
property string instanceId: ""
property bool showMountPath: true
iconName: "storage"
@@ -38,9 +37,6 @@ CompoundPill {
if (!selectedMount) {
return I18n.tr("No disk data");
}
if (!showMountPath) {
return I18n.tr("Disk Usage");
}
return selectedMount.mount;
}
@@ -64,7 +64,7 @@ Rectangle {
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.iconSizeLarge
size: parent.parent.width * 0.25
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
@@ -76,8 +76,8 @@ Rectangle {
StyledText {
text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : ""
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
font.pixelSize: parent.parent.width * 0.15
font.weight: Font.Medium
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error;
@@ -11,7 +11,6 @@ Rectangle {
property string mountPath: "/"
property string instanceId: ""
property bool showMountPath: true
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0)
@@ -68,7 +67,7 @@ Rectangle {
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "storage"
size: Theme.iconSizeLarge
size: Theme.iconSizeSmall
color: {
if (root.usagePercent > 90)
return Theme.error;
@@ -83,7 +82,6 @@ Rectangle {
spacing: 0
StyledText {
visible: root.showMountPath
text: root.selectedMount?.mount || root.mountPath
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -94,7 +92,7 @@ Rectangle {
StyledText {
text: `${root.usagePercent.toFixed(0)}%`
font.pixelSize: root.showMountPath ? Theme.fontSizeSmall : Theme.fontSizeLarge
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: {
if (root.usagePercent > 90)
@@ -1,14 +0,0 @@
function detailHeightForSection(section, maxHeight, pluginInstance) {
if (!section)
return 0;
if (section === "wifi" || section === "bluetooth"
|| section === "builtin_vpn" || section === "builtin_tailscale")
return Math.min(350, maxHeight);
if (section.startsWith("brightnessSlider_"))
return Math.min(400, maxHeight);
if (section.startsWith("plugin_")) {
const h = pluginInstance ? pluginInstance.ccDetailHeight : 0;
return Math.min(h > 0 ? h : 250, maxHeight);
}
return Math.min(250, maxHeight);
}
@@ -1,71 +1,3 @@
function spanWidthFor(baseWidth, widgetWidth, spacing) {
const w = widgetWidth || 50
if (w <= 25)
return (baseWidth - spacing * 3) / 4
if (w <= 50)
return (baseWidth - spacing) / 2
if (w <= 75)
return (baseWidth - spacing * 2) * 0.75
return baseWidth
}
function isSliderWidget(id) {
return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"
}
function computeSlots(widgets, order, baseWidth, spacing, rowSpacing, sliderHeight, normalHeight) {
const slots = []
let x = 0
let y = 0
let rowRight = 0
let rowMaxH = 0
let countInRow = 0
for (let p = 0; p < order.length; p++) {
const sourceIndex = order[p]
const widget = widgets[sourceIndex]
if (!widget)
continue
const itemW = spanWidthFor(baseWidth, widget.width, spacing)
const itemH = isSliderWidget(widget.id || "") ? sliderHeight : normalHeight
if (countInRow > 0 && (rowRight + spacing + itemW > baseWidth + 0.5)) {
y += rowMaxH + rowSpacing
rowRight = 0
rowMaxH = 0
countInRow = 0
}
x = countInRow === 0 ? 0 : rowRight + spacing
slots[sourceIndex] = {
"x": x,
"y": y,
"w": itemW,
"h": itemH
}
rowRight = x + itemW
rowMaxH = Math.max(rowMaxH, itemH)
countInRow++
}
return {
"slots": slots,
"totalHeight": y + rowMaxH
}
}
function slotContainingPoint(slots, order, px, py) {
for (let p = 0; p < order.length; p++) {
const s = slots[order[p]]
if (!s)
continue
if (px >= s.x && px < s.x + s.w && py >= s.y && py < s.y + s.h)
return p
}
return -1
}
function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) {
var rows = []
var currentRow = []
@@ -13,7 +13,6 @@ function addWidget(widgetId) {
if (widgetId === "diskUsage") {
widget.instanceId = generateUniqueId()
widget.mountPath = "/"
widget.showMountPath = true
}
if (widgetId === "brightnessSlider") {
+14 -1
View File
@@ -110,7 +110,20 @@ PanelWindow {
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false)
readonly property var dBarLayer: LayerShell.fromEnv("DMS_DANKBAR_LAYER", barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top)
readonly property var dBarLayer: {
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
case "bottom":
return WlrLayer.Bottom;
case "overlay":
return WlrLayer.Overlay;
case "background":
return WlrLayer.Background;
case "top":
return WlrLayer.Top;
default:
return barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top;
}
}
property var blurRegion: null
property var _blurWidgetItems: []
@@ -21,13 +21,14 @@ DankPopout {
}
function setProfile(profile) {
if (PowerProfileWatcher.applyProfile(profile))
return;
if (!PowerProfileWatcher.available)
if (typeof PowerProfiles === "undefined") {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
else
return;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile) {
ToastService.showError(I18n.tr("Failed to set power profile"));
}
}
popupWidth: 400
@@ -554,7 +555,7 @@ DankPopout {
DankButtonGroup {
id: profileButtonGroup
property var profileModel: PowerProfileWatcher.availableProfiles
property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
property int currentProfileIndex: {
if (typeof PowerProfiles === "undefined")
return 1;
@@ -140,24 +140,30 @@ BasePill {
log.info("Trigger! Delta: " + delta);
// This is after the other delta checks so it only shows on valid Y scroll
if (!PowerProfileWatcher.available) {
if (typeof PowerProfiles === "undefined") {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
return;
}
const profiles = PowerProfileWatcher.availableProfiles;
// Get list of profiles, and current index
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
// Step once based on mouse wheel direction
if (delta > 0)
index += 1;
else
index -= 1;
// Already at end of list, can't go further
if (index < 0 || index >= profiles.length)
return;
if (!PowerProfileWatcher.applyProfile(profiles[index]))
// Set new profile
PowerProfiles.profile = profiles[index];
if (PowerProfiles.profile !== profiles[index]) {
ToastService.showError(I18n.tr("Failed to set power profile"));
}
}
}
}
@@ -26,8 +26,6 @@ BasePill {
property bool showBatteryIcon: widgetData?.showBatteryIcon !== undefined ? widgetData.showBatteryIcon : SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: widgetData?.showPrinterIcon !== undefined ? widgetData.showPrinterIcon : SettingsData.controlCenterShowPrinterIcon
property bool showScreenSharingIcon: widgetData?.showScreenSharingIcon !== undefined ? widgetData.showScreenSharingIcon : SettingsData.controlCenterShowScreenSharingIcon
property bool showIdleInhibitorIcon: widgetData?.showIdleInhibitorIcon !== undefined ? widgetData.showIdleInhibitorIcon : SettingsData.controlCenterShowIdleInhibitorIcon
property bool showDoNotDisturbIcon: widgetData?.showDoNotDisturbIcon !== undefined ? widgetData.showDoNotDisturbIcon : SettingsData.controlCenterShowDoNotDisturbIcon
property real touchpadThreshold: 100
property real micAccumulator: 0
property real volumeAccumulator: 0
@@ -42,7 +40,7 @@ BasePill {
property var _vBrightness: null
property var _vMic: null
property var _interactionDelegates: []
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing", "idleInhibitor", "doNotDisturb"]
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
@@ -355,10 +353,6 @@ BasePill {
return root.showBatteryIcon && BatteryService.batteryAvailable;
case "printer":
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
case "idleInhibitor":
return root.showIdleInhibitorIcon && SessionService.idleInhibited;
case "doNotDisturb":
return root.showDoNotDisturbIcon && SessionData.doNotDisturb;
default:
return false;
}
@@ -518,10 +512,6 @@ BasePill {
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
case "idleInhibitor":
return "motion_sensor_active";
case "doNotDisturb":
return "do_not_disturb_on";
default:
return "settings";
}
@@ -541,10 +531,6 @@ BasePill {
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
case "idleInhibitor":
return Theme.primary;
case "doNotDisturb":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
@@ -703,10 +689,6 @@ BasePill {
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
case "idleInhibitor":
return "motion_sensor_active";
case "doNotDisturb":
return "do_not_disturb_on";
default:
return "settings";
}
@@ -726,10 +708,6 @@ BasePill {
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
case "idleInhibitor":
return Theme.primary;
case "doNotDisturb":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
@@ -10,10 +10,8 @@ BasePill {
property var widgetData: null
property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
property int diskUsageMode: (widgetData && widgetData.diskUsageMode !== undefined) ? widgetData.diskUsageMode : 0
property bool showMountPath: (widgetData && widgetData.showMountPath !== undefined) ? widgetData.showMountPath : true
property bool isHovered: mouseArea.containsMouse
property bool isAutoHideBar: false
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
@@ -71,8 +69,6 @@ BasePill {
}
Connections {
target: SettingsData
function onWidgetDataChanged() {
root.mountPath = Qt.binding(() => {
return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/";
@@ -100,12 +96,14 @@ BasePill {
return DgopService.diskMounts[0] || null;
});
}
target: SettingsData
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : diskContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? diskColumn.implicitHeight : diskContent.implicitHeight
implicitHeight: root.isVerticalOrientation ? diskColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: diskColumn
@@ -120,12 +118,10 @@ BasePill {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger;
}
if (root.diskUsagePercent > 75) {
return Theme.tempWarning;
}
return Theme.widgetIconColor;
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -158,29 +154,24 @@ BasePill {
id: diskContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
spacing: 3
DankIcon {
id: diskIcon
name: "storage"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger;
}
if (root.diskUsagePercent > 75) {
return Theme.tempWarning;
}
return Theme.widgetIconColor;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: mountText
visible: root.showMountPath
text: {
if (!root.selectedMount) {
return "--";
@@ -191,20 +182,32 @@ BasePill {
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideNone
wrapMode: Text.NoWrap
}
Item {
id: textBox
StyledText {
text: {
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
return "--%";
}
if (!root.selectedMount)
return "--%";
switch (root.diskUsageMode) {
case 1:
return root.selectedMount.size || "--";
case 2:
return root.selectedMount.avail || "--";
case 3:
return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
default:
return root.diskUsagePercent.toFixed(0) + "%";
}
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
implicitWidth: root.minimumWidth ? Math.max(diskBaseline.width, diskCurrent.width) : diskCurrent.width
implicitHeight: diskText.implicitHeight
width: implicitWidth
height: implicitHeight
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: diskBaseline
@@ -222,40 +225,7 @@ BasePill {
}
}
StyledTextMetrics {
id: diskCurrent
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
text: diskText.text
}
StyledText {
id: diskText
text: {
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
return "--%";
}
if (!root.selectedMount)
return "--%";
switch (root.diskUsageMode) {
case 1:
return root.selectedMount.size || "--";
case 2:
return root.selectedMount.avail || "--";
case 3:
return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
default:
return root.diskUsagePercent.toFixed(0) + "%";
}
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideNone
wrapMode: Text.NoWrap
}
width: Math.max(diskBaseline.width, paintedWidth)
}
}
}
@@ -11,7 +11,6 @@ BasePill {
property var widgetData: null
property bool compactMode: widgetData?.keyboardLayoutNameCompactMode !== undefined ? widgetData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode
property bool showIcon: widgetData?.keyboardLayoutNameShowIcon !== undefined ? widgetData.keyboardLayoutNameShowIcon : SettingsData.keyboardLayoutNameShowIcon
readonly property var langCodes: ({
"afrikaans": "af",
"albanian": "sq",
@@ -135,7 +134,6 @@ BasePill {
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showIcon
}
StyledText {
@@ -158,14 +156,6 @@ BasePill {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "keyboard"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
visible: root.showIcon
}
StyledText {
text: {
if (!root.currentLayout)
@@ -981,8 +981,6 @@ BasePill {
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (PopoutManager.screenshotActive)
return WlrKeyboardFocus.None;
if (!root.menuOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
@@ -1451,8 +1449,6 @@ BasePill {
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (PopoutManager.screenshotActive)
return WlrKeyboardFocus.None;
if (!menuRoot.showMenu)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
+8 -16
View File
@@ -25,14 +25,14 @@ DankPopout {
property int __dropdownType: 0
property point __dropdownAnchor: Qt.point(0, 0)
property bool __dropdownRightEdge: false
property var __dropdownPlayer: MprisController.activePlayer
property var __dropdownPlayers: MprisController.availablePlayers
property var __dropdownPlayer: null
property var __dropdownPlayers: []
function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = Qt.binding(() => MprisController.activePlayer);
__dropdownPlayers = Qt.binding(() => MprisController.availablePlayers);
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 1;
}
@@ -45,8 +45,8 @@ DankPopout {
function __showPlayersDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = Qt.binding(() => MprisController.activePlayer);
__dropdownPlayers = Qt.binding(() => MprisController.availablePlayers);
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 3;
}
@@ -69,7 +69,7 @@ DankPopout {
id: __volumeCloseTimer
interval: 400
onTriggered: {
if (__dropdownType !== 0) {
if (__dropdownType === 1) {
__hideDropdowns();
}
}
@@ -230,13 +230,6 @@ DankPopout {
return;
}
if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) {
if (mediaLoader.item.handleKeyEvent(event)) {
event.accepted = true;
return;
}
}
if (root.currentTabIndex === 2 && wallpaperLoader.item?.handleKeyEvent) {
if (wallpaperLoader.item.handleKeyEvent(event)) {
event.accepted = true;
@@ -401,8 +394,7 @@ DankPopout {
root.__showPlayersDropdown(pos, rightEdge, player, players);
}
onHideDropdowns: root.__hideDropdowns()
onDropdownButtonExited: root.__startCloseTimer()
onDropdownButtonEntered: root.__stopCloseTimer()
onVolumeButtonExited: root.__startCloseTimer()
}
}
}
@@ -42,22 +42,16 @@ Item {
signal panelEntered
signal panelExited
property int __panelHoverCount: 0
property int __volumeHoverCount: 0
onDropdownTypeChanged: {
if (dropdownType === 0) {
__panelHoverCount = 0;
}
}
function panelAreaEntered() {
__panelHoverCount++;
function volumeAreaEntered() {
__volumeHoverCount++;
panelEntered();
}
function panelAreaExited() {
__panelHoverCount = Math.max(0, __panelHoverCount - 1);
if (__panelHoverCount === 0)
function volumeAreaExited() {
__volumeHoverCount = Math.max(0, __volumeHoverCount - 1);
if (__volumeHoverCount === 0)
panelExited();
}
@@ -137,8 +131,8 @@ Item {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
}
Item {
@@ -196,8 +190,8 @@ Item {
cursorShape: Qt.PointingHandCursor
preventStealing: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => {
if (pressed)
@@ -275,14 +269,6 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
@@ -363,13 +349,7 @@ Item {
}
StyledText {
text: {
if (!modelData?.audio)
return modelData === AudioService.sink ? I18n.tr("Active") : I18n.tr("Available");
if (modelData.audio.muted)
return I18n.tr("Muted", "audio status");
return Math.round(modelData.audio.volume * 100) + "%";
}
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
@@ -389,8 +369,6 @@ Item {
root.deviceSelected(modelData);
}
}
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
}
}
@@ -447,14 +425,6 @@ Item {
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
@@ -528,7 +498,15 @@ Item {
}
StyledText {
text: modelData?.trackArtist || I18n.tr("Unknown Artist")
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
@@ -548,8 +526,6 @@ Item {
root.playerSelected(modelData);
}
}
onEntered: panelAreaEntered()
onExited: panelAreaExited()
}
}
}
+44 -171
View File
@@ -13,7 +13,6 @@ Item {
LayoutMirroring.childrenInherit: true
property MprisPlayer activePlayer: MprisController.activePlayer
readonly property real stableLength: MprisController.activePlayerStableLength
property var allPlayers: MprisController.availablePlayers
property var targetScreen: null
property real popoutX: 0
@@ -28,8 +27,7 @@ Item {
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal hideDropdowns
signal dropdownButtonExited
signal dropdownButtonEntered
signal volumeButtonExited
property bool volumeExpanded: false
property bool devicesExpanded: false
@@ -41,7 +39,9 @@ Item {
playersExpanded = false;
}
DankTooltipV2 {
id: sharedTooltip
}
readonly property bool isRightEdge: {
if (barPosition === SettingsData.Position.Right)
@@ -65,7 +65,8 @@ Item {
// Derived "no players" state: always correct, no timers.
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
readonly property bool _noneAvailable: _playerCount === 0
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || !activePlayer)
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
property bool _switchHold: false
Timer {
@@ -84,6 +85,7 @@ Item {
isSwitching = true;
_switchHold = true;
_switchHoldTimer.restart();
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
}
function maybeFinishSwitch() {
@@ -94,11 +96,11 @@ Item {
}
readonly property real ratio: {
if (!activePlayer || stableLength <= 0) {
if (!activePlayer || !activePlayer.length || activePlayer.length <= 0) {
return 0;
}
const pos = (activePlayer.position || 0) % Math.max(1, stableLength);
const calculatedRatio = pos / stableLength;
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio));
}
@@ -107,11 +109,13 @@ Item {
Connections {
target: activePlayer
ignoreUnknownSignals: true
function onTrackTitleChanged() {
_switchHoldTimer.restart();
maybeFinishSwitch();
}
function onTrackArtUrlChanged() {
TrackArtService.loadArtwork(activePlayer.trackArtUrl);
}
}
Connections {
@@ -182,102 +186,6 @@ Item {
}
}
function triggerVolumeDropdown() {
if (!volumeAvailable)
return;
if (volumeExpanded)
return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
function toggleMute() {
if (!volumeAvailable)
return;
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
function handleKeyEvent(event) {
if (!activePlayer)
return false;
// 1. Number keys 0-9 to seek to 0%-90%
if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
if (activePlayer.canSeek && stableLength > 0) {
const ratio = (event.key - Qt.Key_0) * 0.1;
const targetPosition = ratio * stableLength;
activePlayer.position = Math.max(0.1, Math.min(targetPosition, stableLength * 0.99));
return true;
}
}
// 2. Left / Right arrows to seek backward / forward 5s
if (event.key === Qt.Key_Left) {
if (activePlayer.canSeek) {
activePlayer.position = Math.max(0.1, activePlayer.position - 5);
return true;
}
}
if (event.key === Qt.Key_Right) {
if (activePlayer.canSeek && stableLength > 0) {
activePlayer.position = Math.max(0.1, Math.min(stableLength - 1, activePlayer.position + 5));
return true;
}
}
// 3. Up / Down arrows to adjust volume
if (event.key === Qt.Key_Up) {
adjustVolume(5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
if (event.key === Qt.Key_Down) {
adjustVolume(-5);
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
// 4. Spacebar to play/pause
if (event.key === Qt.Key_Space) {
if (activePlayer.canTogglePlaying) {
activePlayer.togglePlaying();
return true;
}
}
// 5. M key to toggle mute
if (event.key === Qt.Key_M) {
toggleMute();
triggerVolumeDropdown();
dropdownButtonExited();
return true;
}
return false;
}
property bool isSeeking: false
Timer {
@@ -290,14 +198,14 @@ Item {
Item {
id: bgContainer
anchors.fill: parent
visible: TrackArtService.resolvedArtUrl !== ""
visible: TrackArtService._bgArtSource !== ""
Image {
id: bgImage
anchors.centerIn: parent
width: Math.max(parent.width, parent.height) * 1.1
height: width
source: TrackArtService.resolvedArtUrl
source: TrackArtService._bgArtSource
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
@@ -423,7 +331,7 @@ Item {
}
StyledText {
text: activePlayer?.trackArtist || I18n.tr("Unknown Artist")
text: activePlayer?.trackTitle || I18n.tr("Unknown Artist")
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
@@ -481,7 +389,7 @@ Item {
if (!activePlayer)
return "0:00";
const rawPos = Math.max(0, activePlayer.position || 0);
const pos = stableLength ? rawPos % Math.max(1, stableLength) : rawPos;
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
const minutes = Math.floor(pos / 60);
const seconds = Math.floor(pos % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -495,9 +403,9 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: {
if (!activePlayer || stableLength <= 0)
return "--:--";
const dur = stableLength;
if (!activePlayer || !activePlayer.length)
return "0:00";
const dur = Math.max(0, activePlayer.length || 0);
const minutes = Math.floor(dur / 60);
const seconds = Math.floor(dur % 60);
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
@@ -739,17 +647,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (playersExpanded) {
if (allPlayers && allPlayers.length > 1) {
let currentIndex = -1;
for (let i = 0; i < allPlayers.length; i++) {
if (allPlayers[i] === activePlayer) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % allPlayers.length;
MprisController.setActivePlayer(allPlayers[nextIndex]);
}
hideDropdowns();
return;
}
hideDropdowns();
@@ -760,22 +658,8 @@ Item {
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onEntered: {
dropdownButtonEntered();
if (playersExpanded)
return;
hideDropdowns();
playersExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
}
onExited: {
if (playersExpanded)
dropdownButtonExited();
}
onEntered: sharedTooltip.show(I18n.tr("Media Players"), playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
@@ -807,7 +691,6 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
dropdownButtonEntered();
if (volumeExpanded)
return;
hideDropdowns();
@@ -820,10 +703,25 @@ Item {
}
onExited: {
if (volumeExpanded)
dropdownButtonExited();
volumeButtonExited();
}
onClicked: {
toggleMute();
SessionData.suppressOSDTemporarily();
if (currentVolume > 0) {
volumeButton.previousVolume = currentVolume;
if (usePlayerVolume) {
activePlayer.volume = 0;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = 0;
}
} else {
const restoreVolume = volumeButton.previousVolume > 0 ? volumeButton.previousVolume : 0.5;
if (usePlayerVolume) {
activePlayer.volume = restoreVolume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = restoreVolume;
}
}
}
onWheel: wheelEvent => {
SessionData.suppressOSDTemporarily();
@@ -856,7 +754,7 @@ Item {
DankIcon {
anchors.centerIn: parent
name: "speaker"
name: devicesExpanded ? "expand_less" : "speaker"
size: 18
color: Theme.surfaceText
}
@@ -868,18 +766,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
if (devicesExpanded) {
const sinks = AudioService.getAvailableSinks();
if (sinks && sinks.length > 1) {
let currentIndex = -1;
for (let i = 0; i < sinks.length; i++) {
if (sinks[i]?.name === AudioService.sink?.name) {
currentIndex = i;
break;
}
}
const nextIndex = (currentIndex + 1) % sinks.length;
AudioService.setSink(sinks[nextIndex]);
}
hideDropdowns();
return;
}
hideDropdowns();
@@ -890,22 +777,8 @@ Item {
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onEntered: {
dropdownButtonEntered();
if (devicesExpanded)
return;
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
}
onExited: {
if (devicesExpanded)
dropdownButtonExited();
}
onEntered: sharedTooltip.show(I18n.tr("Output Device"), audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
}
@@ -15,11 +15,10 @@ Card {
property real displayPosition: currentPosition
readonly property real ratio: {
const len = MprisController.activePlayerStableLength;
if (!activePlayer || !activePlayer.lengthSupported || len <= 0)
if (!activePlayer || activePlayer.length <= 0)
return 0;
const pos = displayPosition % Math.max(1, len);
const calculatedRatio = pos / len;
const pos = displayPosition % Math.max(1, activePlayer.length);
const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio));
}
@@ -99,9 +99,7 @@ Card {
}
StyledText {
text: DgopService.shortUptime
? I18n.tr("up") + DgopService.shortUptime.slice(2)
: I18n.tr("up")
text: DgopService.shortUptime || I18n.tr("up")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
+1 -20
View File
@@ -18,7 +18,6 @@ Singleton {
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: ""
property string lastSessionExec: ""
property string lastSuccessfulUser: ""
property bool memoryReady: false
property bool isLightMode: false
@@ -55,7 +54,6 @@ Singleton {
return;
const memory = JSON.parse(content);
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSessionExec = rememberLastSession ? (memory.lastSessionExec || "") : "";
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
@@ -68,8 +66,6 @@ Singleton {
let memory = {};
if (rememberLastSession && lastSessionId)
memory.lastSessionId = lastSessionId;
if (rememberLastSession && lastSessionExec)
memory.lastSessionExec = lastSessionExec;
if (rememberLastUser && lastSuccessfulUser)
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
@@ -77,28 +73,13 @@ Singleton {
function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "" || lastSessionExec !== "") {
if (lastSessionId !== "") {
lastSessionId = "";
lastSessionExec = "";
saveMemory();
}
return;
}
lastSessionId = id || "";
if (!lastSessionId)
lastSessionExec = "";
saveMemory();
}
function setLastSessionExec(exec) {
if (!rememberLastSession) {
if (lastSessionExec !== "") {
lastSessionExec = "";
saveMemory();
}
return;
}
lastSessionExec = exec || "";
saveMemory();
}
+7 -19
View File
@@ -12,24 +12,16 @@ Singleton {
id: root
readonly property var log: Log.scoped("GreetdSettings")
readonly property string _greeterCacheDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
property string configBaseDir: root._greeterCacheDir
readonly property string configPath: root.configBaseDir ? (root.configBaseDir + "/settings.json") : ""
readonly property string greeterWallpaperOverridePath: root.configBaseDir ? (root.configBaseDir + "/greeter_wallpaper_override.jpg") : ""
function setConfigBaseDir(dir) {
const next = dir || root._greeterCacheDir;
if (configBaseDir === next)
return;
configBaseDir = next;
settingsLoaded = false;
settingsFile.reload();
readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/settings.json";
}
function resetConfigBaseDir() {
setConfigBaseDir(root._greeterCacheDir);
readonly property string _greeterCacheDir: {
const i = root.configPath.lastIndexOf("/");
return i >= 0 ? root.configPath.substring(0, i) : "";
}
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
property string currentThemeName: "purple"
property bool settingsLoaded: false
@@ -67,7 +59,6 @@ Singleton {
property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterAutoLogin: false
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
@@ -133,9 +124,6 @@ Singleton {
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
if (configBaseDir === root._greeterCacheDir) {
greeterAutoLogin = settings.greeterAutoLogin !== undefined ? settings.greeterAutoLogin : false;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";

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