mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-24 03:55:23 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed731efb0 | |||
| cf0632c077 | |||
| e92da4a15f | |||
| 8abdff3220 | |||
| 584d57a8de | |||
| afb5e59c29 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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] || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ Singleton {
|
||||
property var currentModalsByScreen: ({})
|
||||
|
||||
function openModal(modal) {
|
||||
PopoutManager.screenshotActive = false;
|
||||
const screenName = modal.effectiveScreen?.name ?? "unknown";
|
||||
currentModalsByScreen[screenName] = modal;
|
||||
modalChanged();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "" },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -497,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)
|
||||
|
||||
@@ -251,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)
|
||||
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -352,10 +352,6 @@ QtObject {
|
||||
WidgetUtils.moveWidget(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
function reorderWidgets(newOrder) {
|
||||
WidgetUtils.reorderWidgets(newOrder);
|
||||
}
|
||||
|
||||
function resetToDefault() {
|
||||
WidgetUtils.resetToDefault();
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 : "";
|
||||
|
||||
@@ -57,20 +57,11 @@ Item {
|
||||
property int maxPasswordSessionTransitionRetries: 2
|
||||
property bool fprintdProbeComplete: false
|
||||
property bool fprintdHasDevice: false
|
||||
property bool autoLoginOnSuccess: false
|
||||
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
|
||||
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
|
||||
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
|
||||
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
|
||||
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
|
||||
readonly property bool multipleUsersAvailable: GreeterUsersService.loaded && GreeterUsersService.users.length > 1
|
||||
readonly property bool showUserPicker: multipleUsersAvailable && !GreeterState.showPasswordInput && !manualUsernameEntry
|
||||
readonly property bool showAccountSwitchLink: multipleUsersAvailable && !GreeterState.showPasswordInput && !GreeterState.unlocking
|
||||
readonly property int userPickerMaxHeight: Math.min(400, Math.max(120, height * 0.35))
|
||||
property bool userListOpen: false
|
||||
property bool manualUsernameEntry: false
|
||||
property bool skipAutoSelectUser: false
|
||||
property string pickerThemeUsername: ""
|
||||
|
||||
function initWeatherService() {
|
||||
if (weatherInitialized)
|
||||
@@ -437,87 +428,20 @@ Item {
|
||||
fprintdDeviceProbe.running = true;
|
||||
}
|
||||
|
||||
function applyPickerPreviewTheme() {
|
||||
let previewUser = (pickerThemeUsername || "").trim();
|
||||
if (!previewUser && GreetdSettings.rememberLastUser)
|
||||
previewUser = (GreetdMemory.lastSuccessfulUser || "").trim();
|
||||
if (previewUser)
|
||||
GreeterUserTheme.applyForUser(previewUser);
|
||||
else
|
||||
GreeterUserTheme.applyDefault();
|
||||
}
|
||||
|
||||
function applyLastSuccessfulUser() {
|
||||
if (root.skipAutoSelectUser)
|
||||
return;
|
||||
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||
return;
|
||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||
selectUser(lastUser, true);
|
||||
GreeterState.username = lastUser;
|
||||
GreeterState.usernameInput = lastUser;
|
||||
GreeterState.showPasswordInput = true;
|
||||
PortalService.getGreeterUserProfileImage(lastUser);
|
||||
maybeAutoStartExternalAuth();
|
||||
}
|
||||
}
|
||||
|
||||
function enterManualUsernameEntry() {
|
||||
if (!root.multipleUsersAvailable || GreeterState.showPasswordInput)
|
||||
return;
|
||||
root.manualUsernameEntry = true;
|
||||
root.userListOpen = false;
|
||||
GreeterState.username = "";
|
||||
GreeterState.usernameInput = "";
|
||||
GreeterState.selectedUserIndex = -1;
|
||||
inputField.text = "";
|
||||
root.applyPickerPreviewTheme();
|
||||
Qt.callLater(() => inputField.forceActiveFocus());
|
||||
}
|
||||
|
||||
function returnToUserListFromManualEntry() {
|
||||
if (!root.multipleUsersAvailable)
|
||||
return;
|
||||
root.manualUsernameEntry = false;
|
||||
root.userListOpen = true;
|
||||
GreeterState.username = "";
|
||||
GreeterState.usernameInput = "";
|
||||
inputField.text = "";
|
||||
root.applyPickerPreviewTheme();
|
||||
}
|
||||
|
||||
function returnToUserPicker() {
|
||||
if (!root.multipleUsersAvailable || GreeterState.unlocking)
|
||||
return;
|
||||
root.manualUsernameEntry = false;
|
||||
root.skipAutoSelectUser = true;
|
||||
awaitingExternalAuth = false;
|
||||
pendingPasswordResponse = false;
|
||||
passwordSubmitRequested = false;
|
||||
resetPasswordSessionTransition(true);
|
||||
authTimeout.interval = defaultAuthTimeoutMs;
|
||||
authTimeout.stop();
|
||||
clearAuthFeedback();
|
||||
passwordFailureCount = 0;
|
||||
externalAuthAutoStartedForUser = "";
|
||||
if (Greetd.state !== GreetdState.Inactive)
|
||||
Greetd.cancelSession();
|
||||
const previousUser = GreeterState.username;
|
||||
GreeterState.reset();
|
||||
inputField.text = "";
|
||||
PortalService.profileImage = "";
|
||||
if (previousUser)
|
||||
root.pickerThemeUsername = previousUser;
|
||||
root.applyPickerPreviewTheme();
|
||||
root.userListOpen = true;
|
||||
}
|
||||
|
||||
function selectUser(rawValue, skipDropdownUpdate) {
|
||||
const user = (rawValue || "").trim();
|
||||
if (!user)
|
||||
return;
|
||||
root.manualUsernameEntry = false;
|
||||
root.skipAutoSelectUser = false;
|
||||
submitUsername(user, skipDropdownUpdate === true);
|
||||
}
|
||||
|
||||
function submitUsername(rawValue, skipDropdownUpdate) {
|
||||
function submitUsername(rawValue) {
|
||||
const user = (rawValue || "").trim();
|
||||
if (!user)
|
||||
return;
|
||||
@@ -525,17 +449,9 @@ Item {
|
||||
passwordFailureCount = 0;
|
||||
clearAuthFeedback();
|
||||
externalAuthAutoStartedForUser = "";
|
||||
root.autoLoginOnSuccess = false;
|
||||
}
|
||||
root.pickerThemeUsername = user;
|
||||
GreeterState.username = user;
|
||||
GreeterState.usernameInput = user;
|
||||
GreeterState.showPasswordInput = true;
|
||||
if (!skipDropdownUpdate && typeof GreeterUsersService !== "undefined") {
|
||||
const idx = GreeterUsersService.usernames.indexOf(user);
|
||||
GreeterState.selectedUserIndex = idx;
|
||||
}
|
||||
root.userListOpen = false;
|
||||
PortalService.getGreeterUserProfileImage(user);
|
||||
GreeterState.passwordBuffer = "";
|
||||
pendingPasswordResponse = false;
|
||||
@@ -648,12 +564,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: greeterAutoLoginPendingProcess
|
||||
command: ["sh", "-c", "mkdir -p $(dirname " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending") + ") && touch " + JSON.stringify((Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter") + "/.local/state/auto-login-sync-pending")]
|
||||
running: false
|
||||
}
|
||||
|
||||
Process {
|
||||
id: hyprlandLayoutProcess
|
||||
running: false
|
||||
@@ -727,44 +637,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GreeterUsersService
|
||||
function onLoadedChanged() {
|
||||
if (GreeterUsersService.loaded && isPrimaryScreen)
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
function onSyncedThemePathsChanged() {
|
||||
if (!isPrimaryScreen)
|
||||
return;
|
||||
if (GreeterState.username)
|
||||
GreeterUserTheme.applyForUser(GreeterState.username);
|
||||
else if (root.showUserPicker || root.userListOpen)
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GreeterState
|
||||
function onUsernameChanged() {
|
||||
if (GreeterState.username) {
|
||||
root.pickerThemeUsername = GreeterState.username;
|
||||
GreeterUserTheme.applyForUser(GreeterState.username);
|
||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
||||
} else if (root.showUserPicker || root.userListOpen) {
|
||||
applyPickerPreviewTheme();
|
||||
}
|
||||
}
|
||||
function onShowPasswordInputChanged() {
|
||||
if (GreeterState.showPasswordInput)
|
||||
root.userListOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
onShowUserPickerChanged: {
|
||||
if (showUserPicker && !GreeterState.username)
|
||||
applyPickerPreviewTheme();
|
||||
if (!showUserPicker)
|
||||
userListOpen = false;
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -857,245 +736,177 @@ Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
Column {
|
||||
id: greeterMainColumn
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
width: 380
|
||||
anchors.bottom: parent.verticalCenter
|
||||
anchors.bottomMargin: 60
|
||||
width: parent.width
|
||||
height: clockText.implicitHeight
|
||||
|
||||
Item {
|
||||
id: clockContainer
|
||||
Row {
|
||||
id: clockText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: 0
|
||||
|
||||
width: parent.width
|
||||
height: clockText.implicitHeight
|
||||
property string fullTimeStr: {
|
||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||
}
|
||||
property var timeParts: fullTimeStr.split(':')
|
||||
property string hours: timeParts[0] || ""
|
||||
property string minutes: timeParts[1] || ""
|
||||
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
|
||||
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
|
||||
property string ampm: {
|
||||
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
|
||||
return match ? match[0].trim() : "";
|
||||
}
|
||||
property bool hasSeconds: timeParts.length > 2
|
||||
|
||||
Row {
|
||||
id: clockText
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
spacing: 0
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
property string fullTimeStr: {
|
||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||
}
|
||||
property var timeParts: fullTimeStr.split(':')
|
||||
property string hours: timeParts[0] || ""
|
||||
property string minutes: timeParts[1] || ""
|
||||
property string secondsWithAmPm: timeParts.length > 2 ? timeParts[2] : ""
|
||||
property string seconds: secondsWithAmPm.replace(/\s*(AM|PM|am|pm)$/i, '')
|
||||
property string ampm: {
|
||||
const match = fullTimeStr.match(/\s*(AM|PM|am|pm)$/i);
|
||||
return match ? match[0].trim() : "";
|
||||
}
|
||||
property bool hasSeconds: timeParts.length > 2
|
||||
StyledText {
|
||||
text: ":"
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hours.length > 1 ? clockText.hours[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hours.length > 1 ? clockText.hours[1] : clockText.hours.length > 0 ? clockText.hours[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: ":"
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
}
|
||||
StyledText {
|
||||
text: clockText.hasSeconds ? ":" : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.minutes.length > 0 ? clockText.minutes[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.minutes.length > 1 ? clockText.minutes[1] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: clockText.hasSeconds ? ":" : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
StyledText {
|
||||
width: 20
|
||||
text: " "
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.ampm !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hasSeconds && clockText.seconds.length > 0 ? clockText.seconds[0] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 75
|
||||
text: clockText.hasSeconds && clockText.seconds.length > 1 ? clockText.seconds[1] : ""
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: clockText.hasSeconds
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: 20
|
||||
text: " "
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.ampm !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: clockText.ampm
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.ampm !== ""
|
||||
}
|
||||
StyledText {
|
||||
text: clockText.ampm
|
||||
font.pixelSize: 120
|
||||
font.weight: Font.Light
|
||||
color: "white"
|
||||
visible: clockText.ampm !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: dateText
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat())
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
opacity: 0.9
|
||||
StyledText {
|
||||
id: dateText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: clockContainer.bottom
|
||||
anchors.topMargin: 4
|
||||
text: {
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
opacity: 0.9
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: userPickerHint
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: root.showUserPicker && !GreeterState.showPasswordInput && !GreeterState.username && !root.userListOpen
|
||||
text: I18n.tr("Select user...", "greeter user picker placeholder")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: "white"
|
||||
opacity: 0.85
|
||||
}
|
||||
Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: dateText.bottom
|
||||
anchors.topMargin: Theme.spacingL
|
||||
width: 380
|
||||
height: 140
|
||||
|
||||
ColumnLayout {
|
||||
id: authColumn
|
||||
|
||||
width: parent.width
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
RowLayout {
|
||||
spacing: Theme.spacingL
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
DankCircularImage {
|
||||
Layout.preferredWidth: 60
|
||||
Layout.preferredHeight: 60
|
||||
visible: GreetdSettings.lockScreenShowProfileImage || root.multipleUsersAvailable
|
||||
|
||||
DankCircularImage {
|
||||
anchors.fill: parent
|
||||
imageSource: {
|
||||
const displayUser = GreeterState.username || root.pickerThemeUsername;
|
||||
if (displayUser) {
|
||||
const cachedPath = GreeterUsersService.profileImagePath(displayUser);
|
||||
if (cachedPath)
|
||||
return encodeFileUrl(cachedPath);
|
||||
}
|
||||
if (PortalService.profileImage === "")
|
||||
return "";
|
||||
if (PortalService.profileImage.startsWith("/"))
|
||||
return encodeFileUrl(PortalService.profileImage);
|
||||
return PortalService.profileImage;
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: avatarPickerArea.containsMouse || root.userListOpen ? 2 : 0
|
||||
visible: root.multipleUsersAvailable
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: avatarPickerArea
|
||||
|
||||
anchors.fill: parent
|
||||
visible: root.multipleUsersAvailable
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (GreeterState.showPasswordInput)
|
||||
root.returnToUserPicker();
|
||||
else if (root.manualUsernameEntry)
|
||||
root.returnToUserListFromManualEntry();
|
||||
else
|
||||
root.userListOpen = !root.userListOpen;
|
||||
}
|
||||
imageSource: {
|
||||
if (PortalService.profileImage === "")
|
||||
return "";
|
||||
if (PortalService.profileImage.startsWith("/"))
|
||||
return encodeFileUrl(PortalService.profileImage);
|
||||
return PortalService.profileImage;
|
||||
}
|
||||
fallbackIcon: "person"
|
||||
visible: GreetdSettings.lockScreenShowProfileImage
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool showPassword: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.showUserPicker && root.userListOpen ? Math.max(60, userPicker.implicitHeight + Theme.spacingM * 2) : 60
|
||||
|
||||
Layout.preferredHeight: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.9)
|
||||
border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
|
||||
border.width: inputField.activeFocus ? 2 : 1
|
||||
|
||||
GreeterUserPicker {
|
||||
id: userPicker
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: root.userListOpen ? undefined : parent.verticalCenter
|
||||
anchors.top: root.userListOpen ? parent.top : undefined
|
||||
anchors.margins: Theme.spacingM
|
||||
maxExpandedHeight: root.userPickerMaxHeight
|
||||
visible: root.showUserPicker && !GreeterState.showPasswordInput
|
||||
expanded: root.userListOpen
|
||||
onUserSelected: username => root.selectUser(username, false)
|
||||
onToggleRequested: root.userListOpen = !root.userListOpen
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: lockIcon
|
||||
|
||||
@@ -1105,7 +916,6 @@ Item {
|
||||
name: GreeterState.showPasswordInput ? "lock" : "person"
|
||||
size: 20
|
||||
color: inputField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||
visible: !root.showUserPicker
|
||||
}
|
||||
|
||||
TextInput {
|
||||
@@ -1131,9 +941,8 @@ Item {
|
||||
}
|
||||
return margin;
|
||||
}
|
||||
enabled: !root.showUserPicker || GreeterState.showPasswordInput
|
||||
opacity: 0
|
||||
focus: !root.showUserPicker || GreeterState.showPasswordInput
|
||||
focus: true
|
||||
echoMode: GreeterState.showPasswordInput ? (parent.showPassword ? TextInput.Normal : TextInput.Password) : TextInput.Normal
|
||||
onTextChanged: {
|
||||
if (syncingFromState)
|
||||
@@ -1196,14 +1005,11 @@ Item {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
return I18n.tr("Password...");
|
||||
}
|
||||
if (root.showUserPicker) {
|
||||
return "";
|
||||
}
|
||||
return I18n.tr("Username...");
|
||||
}
|
||||
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length === 0)) ? 1 : 0
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
@@ -1237,7 +1043,7 @@ Item {
|
||||
}
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: (GreeterState.showPasswordInput && !parent.showPassword) ? Theme.fontSizeLarge : Theme.fontSizeMedium
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : (root.showUserPicker ? false : GreeterState.usernameInput.length > 0)) ? 1 : 0
|
||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length > 0 : GreeterState.usernameInput.length > 0) ? 1 : 0
|
||||
clip: true
|
||||
elide: Text.ElideNone
|
||||
horizontalAlignment: implicitWidth > width ? Text.AlignRight : Text.AlignLeft
|
||||
@@ -1282,7 +1088,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput)
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboard_controller.isKeyboardActive) {
|
||||
@@ -1301,7 +1107,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking && (!root.showUserPicker || GreeterState.showPasswordInput)
|
||||
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (GreeterState.showPasswordInput) {
|
||||
@@ -1331,39 +1137,9 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.showAccountSwitchLink ? 28 : 0
|
||||
visible: root.showAccountSwitchLink
|
||||
|
||||
StyledText {
|
||||
id: accountSwitchLabel
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.manualUsernameEntry ? I18n.tr("Back to user list", "greeter link to return from manual username entry to user picker") : I18n.tr("Not listed?", "greeter link to switch to manual username entry")
|
||||
color: Theme.primary
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.underline: accountSwitchMouse.containsMouse
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: accountSwitchMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.manualUsernameEntry)
|
||||
root.returnToUserListFromManualEntry();
|
||||
else
|
||||
root.enterManualUsernameEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.authFeedbackMessage !== "" ? 38 : 0
|
||||
Layout.preferredHeight: 38
|
||||
Layout.topMargin: -Theme.spacingS
|
||||
Layout.bottomMargin: -Theme.spacingS
|
||||
text: root.authFeedbackMessage
|
||||
@@ -1382,149 +1158,52 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Password-screen actions: Switch User + Auto-login toggle as one compact chip row
|
||||
Item {
|
||||
id: passwordActions
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 0
|
||||
Layout.preferredWidth: switchUserRow.width + Theme.spacingL * 2
|
||||
Layout.preferredHeight: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: GreeterState.showPasswordInput ? 1 : 0
|
||||
enabled: GreeterState.showPasswordInput
|
||||
|
||||
readonly property bool autoLoginAvailable: GreetdSettings.rememberLastUser && GreetdSettings.rememberLastSession
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Theme.spacingXS
|
||||
Layout.preferredHeight: visible ? 32 : 0
|
||||
visible: GreeterState.showPasswordInput && !GreeterState.unlocking && (root.multipleUsersAvailable || autoLoginAvailable)
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: switchUserRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: switchUserChip
|
||||
|
||||
visible: root.multipleUsersAvailable
|
||||
height: 32
|
||||
width: switchUserContent.implicitWidth + Theme.spacingM * 2
|
||||
radius: height / 2
|
||||
color: Theme.withAlpha(Theme.surfaceVariant, 0.65)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: (switchUserMouse.containsMouse || switchUserMouse.pressed) ? Theme.surfaceTextHover : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: switchUserRipple
|
||||
cornerRadius: switchUserChip.radius
|
||||
rippleColor: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Row {
|
||||
id: switchUserContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "people"
|
||||
size: 16
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Switch User")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: switchUserMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => switchUserRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: root.returnToUserPicker()
|
||||
}
|
||||
DankIcon {
|
||||
name: "people"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: autoLoginChip
|
||||
StyledText {
|
||||
text: I18n.tr("Switch User")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
visible: passwordActions.autoLoginAvailable
|
||||
height: 32
|
||||
width: autoLoginContent.implicitWidth + Theme.spacingM * 2
|
||||
radius: height / 2
|
||||
color: root.autoLoginOnSuccess ? Theme.withAlpha(Theme.primary, 0.85) : Theme.withAlpha(Theme.surfaceVariant, 0.65)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (autoLoginMouse.pressed)
|
||||
return root.autoLoginOnSuccess ? Theme.primaryPressed : Theme.surfaceTextHover;
|
||||
if (autoLoginMouse.containsMouse)
|
||||
return root.autoLoginOnSuccess ? Theme.primaryHover : Theme.surfaceTextHover;
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: autoLoginRipple
|
||||
cornerRadius: autoLoginChip.radius
|
||||
rippleColor: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Row {
|
||||
id: autoLoginContent
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: root.autoLoginOnSuccess ? "check" : "login"
|
||||
size: 16
|
||||
color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Auto-login")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: root.autoLoginOnSuccess ? Font.Medium : Font.Normal
|
||||
color: root.autoLoginOnSuccess ? Theme.primaryText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: autoLoginMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.autoLoginOnSuccess = !root.autoLoginOnSuccess
|
||||
onPressed: mouse => autoLoginRipple.trigger(mouse.x, mouse.y)
|
||||
}
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||
onClicked: {
|
||||
GreeterState.reset();
|
||||
root.externalAuthAutoStartedForUser = "";
|
||||
inputField.text = "";
|
||||
PortalService.profileImage = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2094,8 +1773,7 @@ Item {
|
||||
launchTimeout.restart();
|
||||
if (GreetdSettings.rememberLastSession) {
|
||||
GreetdMemory.setLastSessionId(sessionPath);
|
||||
GreetdMemory.setLastSessionExec(sessionCmd);
|
||||
} else if (GreetdMemory.lastSessionId || GreetdMemory.lastSessionExec) {
|
||||
} else if (GreetdMemory.lastSessionId) {
|
||||
GreetdMemory.setLastSessionId("");
|
||||
}
|
||||
if (GreetdSettings.rememberLastUser) {
|
||||
@@ -2103,8 +1781,6 @@ Item {
|
||||
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||
GreetdMemory.setLastSuccessfulUser("");
|
||||
}
|
||||
if (root.autoLoginOnSuccess)
|
||||
greeterAutoLoginPendingProcess.running = true;
|
||||
pendingLaunchCommand = sessionCmd;
|
||||
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
|
||||
memoryFlushTimer.restart();
|
||||
|
||||
@@ -19,8 +19,6 @@ Singleton {
|
||||
property var sessionExecs: []
|
||||
property var sessionPaths: []
|
||||
property int currentSessionIndex: 0
|
||||
property var availableUsers: []
|
||||
property int selectedUserIndex: -1
|
||||
|
||||
function reset() {
|
||||
showPasswordInput = false;
|
||||
@@ -28,6 +26,5 @@ Singleton {
|
||||
usernameInput = "";
|
||||
passwordBuffer = "";
|
||||
pamState = "";
|
||||
selectedUserIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool expanded: false
|
||||
property int maxExpandedHeight: 400
|
||||
|
||||
signal userSelected(string username)
|
||||
signal toggleRequested()
|
||||
|
||||
readonly property int rowHeight: 52
|
||||
readonly property int collapsedBarHeight: 36
|
||||
readonly property int expandedListHeight: {
|
||||
if (!expanded)
|
||||
return 0;
|
||||
const count = GreeterUsersService.users.length;
|
||||
if (count === 0)
|
||||
return 0;
|
||||
const fullHeight = count * rowHeight + Math.max(0, count - 1) * Theme.spacingXS;
|
||||
return Math.min(maxExpandedHeight, fullHeight);
|
||||
}
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
return "file://" + path.split("/").map(s => encodeURIComponent(s)).join("/");
|
||||
}
|
||||
|
||||
function profileImageSource(username) {
|
||||
const path = GreeterUsersService.profileImagePath(username);
|
||||
if (path)
|
||||
return encodeFileUrl(path);
|
||||
return "";
|
||||
}
|
||||
|
||||
implicitHeight: expanded ? expandedListHeight : collapsedBarHeight
|
||||
implicitWidth: parent ? parent.width : 320
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: expanded ? undefined : parent.verticalCenter
|
||||
height: collapsedBarHeight
|
||||
visible: !expanded && !!GreeterState.username
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: GreeterUsersService.optionLabel(GreeterState.username)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: collapsedBarHeight
|
||||
visible: !expanded && !GreeterState.username
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleRequested()
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: userListView
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: expandedListHeight
|
||||
visible: expanded
|
||||
clip: true
|
||||
interactive: contentHeight > height
|
||||
spacing: Theme.spacingXS
|
||||
model: GreeterUsersService.users
|
||||
|
||||
delegate: Rectangle {
|
||||
id: userRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: userListView.width
|
||||
height: root.rowHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: userRowMouse.containsMouse ? Theme.surfacePressed : "transparent"
|
||||
border.color: GreeterState.username === userRow.modelData.username ? Theme.primary : "transparent"
|
||||
border.width: GreeterState.username === userRow.modelData.username ? 1 : 0
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: 36
|
||||
Layout.preferredHeight: 36
|
||||
|
||||
DankCircularImage {
|
||||
anchors.fill: parent
|
||||
imageSource: root.profileImageSource(userRow.modelData.username)
|
||||
fallbackIcon: "person"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: GreeterUsersService.optionLabel(userRow.modelData.username)
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: userRowMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.userSelected(userRow.modelData.username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user