mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-28 05:55:21 -04:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed731efb0 | |||
| cf0632c077 | |||
| e92da4a15f | |||
| 8abdff3220 | |||
| 584d57a8de | |||
| afb5e59c29 | |||
| d9525908f1 | |||
| 6093c37b41 | |||
| bb05cbb6c5 | |||
| 4d4af8f549 | |||
| 0b55fbcb15 | |||
| 7476a220b5 | |||
| aaff1ab61e | |||
| 39622eb62a | |||
| eea039f575 | |||
| ef5de19f6b | |||
| f0c31bd7b3 | |||
| 7ddd0ca90d | |||
| b84e5abc4a | |||
| fb9ec8e721 | |||
| 078c9b4890 | |||
| 37c98220a9 | |||
| fc07611b3b | |||
| a923308c09 | |||
| 0990b43a43 | |||
| 548c2305fb | |||
| 4634763840 | |||
| cdc1102092 | |||
| 4845299cc2 | |||
| 81a1bb1cd7 | |||
| 4528552610 | |||
| 0b55bf5dac | |||
| 8dd891f93a | |||
| 9bd68d44a1 | |||
| 90ea136379 | |||
| 2f4a39f9eb | |||
| 5e558660c3 |
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
@@ -37,6 +38,7 @@ var runCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.ApplyEnvOverrides()
|
log.ApplyEnvOverrides()
|
||||||
|
config.CleanupStrayHyprlandConfFile(log.Infof)
|
||||||
if daemon {
|
if daemon {
|
||||||
runShellDaemon(session)
|
runShellDaemon(session)
|
||||||
} else {
|
} else {
|
||||||
@@ -539,5 +541,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
blurCmd,
|
blurCmd,
|
||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
|
switchUserCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -27,7 +28,21 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
case 0:
|
case 0:
|
||||||
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
case 1:
|
case 1:
|
||||||
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{
|
||||||
|
"binds.lua",
|
||||||
|
"binds-user.lua",
|
||||||
|
"colors.lua",
|
||||||
|
"layout.lua",
|
||||||
|
"outputs.lua",
|
||||||
|
"cursor.lua",
|
||||||
|
"windowrules.lua",
|
||||||
|
"cursor.kdl",
|
||||||
|
"outputs.kdl",
|
||||||
|
"binds.kdl",
|
||||||
|
"cursor.conf",
|
||||||
|
"outputs.conf",
|
||||||
|
"binds.conf",
|
||||||
|
}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -82,17 +97,35 @@ func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
|||||||
result.Exists = true
|
result.Exists = true
|
||||||
}
|
}
|
||||||
|
|
||||||
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
targetAbs, err := filepath.Abs(targetPath)
|
||||||
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
if err != nil {
|
||||||
return result, nil
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRel := filepath.ToSlash(filepath.Join("dms", filename))
|
||||||
|
|
||||||
|
mainLua := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
if _, err := os.Stat(mainLua); err == nil {
|
||||||
|
processedLua := make(map[string]bool)
|
||||||
|
if luaconfig.RequiresTarget(mainLua, targetAbs, processedLua) {
|
||||||
|
result.Included = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConf := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConf); err == nil {
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
if hyprlandFindIncludeHyprlang(mainConf, targetRel, processed) {
|
||||||
|
result.Included = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processed := make(map[string]bool)
|
|
||||||
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]bool) bool {
|
||||||
absPath, err := filepath.Abs(filePath)
|
absPath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
@@ -141,7 +174,7 @@ func hyprlandFindInclude(filePath, target string, processed map[string]bool) boo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if hyprlandFindInclude(expanded, target, processed) {
|
if hyprlandFindIncludeHyprlang(expanded, target, processed) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,20 @@ var keybindsSetCmd = &cobra.Command{
|
|||||||
|
|
||||||
var keybindsRemoveCmd = &cobra.Command{
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
Use: "remove <provider> <key>",
|
Use: "remove <provider> <key>",
|
||||||
Short: "Remove a keybind override",
|
Short: "Remove a keybind",
|
||||||
Long: "Remove a keybind override from the specified provider",
|
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runKeybindsRemove,
|
Run: runKeybindsRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var keybindsResetCmd = &cobra.Command{
|
||||||
|
Use: "reset <provider> <key>",
|
||||||
|
Short: "Reset a keybind override to its DMS default",
|
||||||
|
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runKeybindsReset,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
@@ -72,6 +80,7 @@ func init() {
|
|||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
keybindsCmd.AddCommand(keybindsSetCmd)
|
keybindsCmd.AddCommand(keybindsSetCmd)
|
||||||
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
keybindsCmd.AddCommand(keybindsRemoveCmd)
|
||||||
|
keybindsCmd.AddCommand(keybindsResetCmd)
|
||||||
|
|
||||||
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) {
|
||||||
return providers.NewJSONFileProvider(filePath)
|
return providers.NewJSONFileProvider(filePath)
|
||||||
@@ -263,3 +272,19 @@ func runKeybindsRemove(_ *cobra.Command, args []string) {
|
|||||||
}, "", " ")
|
}, "", " ")
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runKeybindsReset(_ *cobra.Command, args []string) {
|
||||||
|
providerName, key := args[0], args[1]
|
||||||
|
writable := getWritableProvider(providerName)
|
||||||
|
|
||||||
|
if err := writable.ResetBind(key); err != nil {
|
||||||
|
log.Fatalf("Error resetting keybind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.MarshalIndent(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"key": key,
|
||||||
|
"reset": true,
|
||||||
|
}, "", " ")
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var switchUserCmd = &cobra.Command{
|
||||||
|
Use: "switch-user [target]",
|
||||||
|
Short: "Switch to another active session on this seat",
|
||||||
|
Long: `Switch the active VT to another running session.
|
||||||
|
|
||||||
|
With no target, prints the list of switchable sessions. Pass a username or a
|
||||||
|
numeric session ID to switch directly. Requires the target to already be a
|
||||||
|
running session on the same seat (use the greeter for a fresh login).`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runSwitchUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Seat string
|
||||||
|
TTY string
|
||||||
|
Type string
|
||||||
|
Class string
|
||||||
|
Active bool
|
||||||
|
State string
|
||||||
|
Current bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSwitchUser(cmd *cobra.Command, args []string) {
|
||||||
|
currentID := os.Getenv("XDG_SESSION_ID")
|
||||||
|
sessions, err := listSessions(currentID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switchable := make([]sessionInfo, 0, len(sessions))
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.Class != "user" || s.State == "closing" || s.Current {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switchable = append(switchable, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
if len(switchable) == 0 {
|
||||||
|
fmt.Println("No other active sessions on this seat.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printSessions(switchable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
picked, err := pickSession(switchable, target)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
if len(switchable) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "No other active sessions on this seat. Only already-running sessions can be switched to.")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "\nSwitchable sessions:")
|
||||||
|
printSessions(switchable)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := activateSession(picked.ID); err != nil {
|
||||||
|
log.Fatalf("loginctl activate %s: %v", picked.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listSessions(currentID string) ([]sessionInfo, error) {
|
||||||
|
listOut, err := exec.Command("loginctl", "list-sessions", "--no-legend").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("loginctl list-sessions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(string(listOut)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]sessionInfo, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
s, err := showSession(id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Current = currentID != "" && s.ID == currentID
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
sort.SliceStable(out, func(i, j int) bool {
|
||||||
|
if out[i].Name != out[j].Name {
|
||||||
|
return out[i].Name < out[j].Name
|
||||||
|
}
|
||||||
|
return out[i].ID < out[j].ID
|
||||||
|
})
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSession(id string) (sessionInfo, error) {
|
||||||
|
out, err := exec.Command("loginctl", "show-session", id,
|
||||||
|
"-p", "Id", "-p", "Name", "-p", "Seat", "-p", "TTY",
|
||||||
|
"-p", "Type", "-p", "Class", "-p", "Active", "-p", "State").Output()
|
||||||
|
if err != nil {
|
||||||
|
return sessionInfo{}, err
|
||||||
|
}
|
||||||
|
fields := map[string]string{}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
idx := strings.IndexByte(line, '=')
|
||||||
|
if idx <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields[line[:idx]] = line[idx+1:]
|
||||||
|
}
|
||||||
|
if fields["Id"] == "" {
|
||||||
|
return sessionInfo{}, fmt.Errorf("session %s: no Id", id)
|
||||||
|
}
|
||||||
|
return sessionInfo{
|
||||||
|
ID: fields["Id"],
|
||||||
|
Name: fields["Name"],
|
||||||
|
Seat: fields["Seat"],
|
||||||
|
TTY: fields["TTY"],
|
||||||
|
Type: fields["Type"],
|
||||||
|
Class: fields["Class"],
|
||||||
|
Active: fields["Active"] == "yes",
|
||||||
|
State: fields["State"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickSession(sessions []sessionInfo, target string) (sessionInfo, error) {
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.ID == target {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matches := make([]sessionInfo, 0, 2)
|
||||||
|
for _, s := range sessions {
|
||||||
|
if s.Name == target {
|
||||||
|
matches = append(matches, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(matches) == 1 {
|
||||||
|
return matches[0], nil
|
||||||
|
}
|
||||||
|
if len(matches) > 1 {
|
||||||
|
ids := make([]string, len(matches))
|
||||||
|
for i, m := range matches {
|
||||||
|
ids[i] = m.ID
|
||||||
|
}
|
||||||
|
return sessionInfo{}, fmt.Errorf("%s has multiple active sessions (%s); pass a session ID instead", target, strings.Join(ids, ", "))
|
||||||
|
}
|
||||||
|
return sessionInfo{}, fmt.Errorf("no switchable session matches %q", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateSession(id string) error {
|
||||||
|
return exec.Command("loginctl", "activate", id).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSessions(sessions []sessionInfo) {
|
||||||
|
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", "ID", "USER", "TYPE", "SEAT", "TTY")
|
||||||
|
for _, s := range sessions {
|
||||||
|
tty := s.TTY
|
||||||
|
if tty == "" {
|
||||||
|
tty = "-"
|
||||||
|
}
|
||||||
|
seat := s.Seat
|
||||||
|
if seat == "" {
|
||||||
|
seat = "-"
|
||||||
|
}
|
||||||
|
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", s.ID, s.Name, s.Type, seat, tty)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,25 +109,25 @@ type dmsConfigSpec struct {
|
|||||||
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||||
"binds": {
|
"binds": {
|
||||||
niriFile: "binds.kdl",
|
niriFile: "binds.kdl",
|
||||||
hyprFile: "binds.conf",
|
hyprFile: "binds.lua",
|
||||||
niriContent: func(t string) string {
|
niriContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
hyprContent: func(t string) string {
|
hyprContent: func(t string) string {
|
||||||
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
return strings.ReplaceAll(config.DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
niriFile: "layout.kdl",
|
niriFile: "layout.kdl",
|
||||||
hyprFile: "layout.conf",
|
hyprFile: "layout.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig },
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
niriFile: "colors.kdl",
|
niriFile: "colors.kdl",
|
||||||
hyprFile: "colors.conf",
|
hyprFile: "colors.lua",
|
||||||
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
hyprContent: func(_ string) string { return config.DMSColorsLuaConfig },
|
||||||
},
|
},
|
||||||
"alttab": {
|
"alttab": {
|
||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
@@ -135,21 +135,21 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
},
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.conf",
|
hyprFile: "outputs.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSOutputsLuaConfig },
|
||||||
},
|
},
|
||||||
"cursor": {
|
"cursor": {
|
||||||
niriFile: "cursor.kdl",
|
niriFile: "cursor.kdl",
|
||||||
hyprFile: "cursor.conf",
|
hyprFile: "cursor.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSCursorLuaConfig },
|
||||||
},
|
},
|
||||||
"windowrules": {
|
"windowrules": {
|
||||||
niriFile: "windowrules.kdl",
|
niriFile: "windowrules.kdl",
|
||||||
hyprFile: "windowrules.conf",
|
hyprFile: "windowrules.lua",
|
||||||
niriContent: func(_ string) string { return "" },
|
niriContent: func(_ string) string { return "" },
|
||||||
hyprContent: func(_ string) string { return "" },
|
hyprContent: func(_ string) string { return config.DMSWindowRulesLuaConfig },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,16 +438,22 @@ func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.
|
|||||||
willBackup := false
|
willBackup := false
|
||||||
|
|
||||||
if wmSelected {
|
if wmSelected {
|
||||||
var configPath string
|
var configPaths []string
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
|
configPaths = []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
|
configPaths = []string{
|
||||||
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
||||||
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
for _, configPath := range configPaths {
|
||||||
willBackup = true
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
willBackup = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ var windowrulesListCmd = &cobra.Command{
|
|||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -40,8 +40,7 @@ var windowrulesAddCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -55,7 +54,7 @@ var windowrulesUpdateCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -69,7 +68,7 @@ var windowrulesRemoveCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -83,7 +82,7 @@ var windowrulesReorderCmd = &cobra.Command{
|
|||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
},
|
},
|
||||||
@@ -118,9 +117,9 @@ func getCompositor(args []string) string {
|
|||||||
if os.Getenv("NIRI_SOCKET") != "" {
|
if os.Getenv("NIRI_SOCKET") != "" {
|
||||||
return "niri"
|
return "niri"
|
||||||
}
|
}
|
||||||
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
// return "hyprland"
|
return "hyprland"
|
||||||
// }
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +182,6 @@ func runWindowrulesList(cmd *cobra.Command, args []string) {
|
|||||||
result.DMSStatus = parseResult.DMSStatus
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
case "hyprland":
|
case "hyprland":
|
||||||
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
|
||||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||||
|
|||||||
+179
-110
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const hyprlandBackupDirName = ".dms-backups"
|
||||||
|
|
||||||
type ConfigDeployer struct {
|
type ConfigDeployer struct {
|
||||||
logChan chan<- string
|
logChan chan<- string
|
||||||
}
|
}
|
||||||
@@ -63,12 +65,23 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
// Primary config file paths used to detect fresh installs.
|
// Primary config file paths used to detect fresh installs.
|
||||||
configPrimaryPaths := map[string]string{
|
configPrimaryPaths := map[string][]string{
|
||||||
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
"Niri": {
|
||||||
"Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
"Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
},
|
||||||
"Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
"Hyprland": {
|
||||||
"Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
|
},
|
||||||
|
"Ghostty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||||
|
},
|
||||||
|
"Kitty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||||
|
},
|
||||||
|
"Alacritty": {
|
||||||
|
filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
@@ -81,8 +94,15 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
}
|
}
|
||||||
// Config is explicitly set to "don't replace" — but still deploy
|
// Config is explicitly set to "don't replace" — but still deploy
|
||||||
// if the config file doesn't exist yet (fresh install scenario).
|
// if the config file doesn't exist yet (fresh install scenario).
|
||||||
if primaryPath, ok := configPrimaryPaths[configType]; ok {
|
if primaryPaths, ok := configPrimaryPaths[configType]; ok {
|
||||||
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
|
exists := false
|
||||||
|
for _, primaryPath := range primaryPaths {
|
||||||
|
if _, err := os.Stat(primaryPath); err == nil {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +515,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
|||||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Hyprland",
|
ConfigType: "Hyprland",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua"),
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
@@ -510,20 +530,20 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
backupDir := filepath.Join(configDir, hyprlandBackupDirName, timestamp)
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
existingData, existingPath, err := readExistingHyprlandConfig(configDir)
|
||||||
cd.log("Found existing Hyprland configuration")
|
if err != nil {
|
||||||
|
result.Error = err
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
if existingData != "" {
|
||||||
|
existingConfig = existingData
|
||||||
|
cd.log(fmt.Sprintf("Found existing Hyprland configuration at %s", existingPath))
|
||||||
|
|
||||||
existingData, err := os.ReadFile(result.Path)
|
result.BackupPath = filepath.Join(backupDir, filepath.Base(existingPath))
|
||||||
if err != nil {
|
if err := backupHyprlandConfigFile(existingPath, result.BackupPath, []byte(existingData), strings.EqualFold(filepath.Ext(existingPath), ".conf")); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
|
||||||
return result, result.Error
|
|
||||||
}
|
|
||||||
existingConfig = string(existingData)
|
|
||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0o644); err != nil {
|
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -542,10 +562,10 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
terminalCommand = "ghostty"
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
newConfig := strings.ReplaceAll(HyprlandLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
|
||||||
if !useSystemd {
|
if !useSystemd {
|
||||||
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
|
newConfig = transformHyprlandLuaForNonSystemd(newConfig, terminalCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
@@ -563,6 +583,18 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
movedLegacy, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to back up legacy hyprlang configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
if movedLegacy > 0 {
|
||||||
|
if result.BackupPath == "" {
|
||||||
|
result.BackupPath = backupDir
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Moved %d legacy hyprlang config(s) to %s", movedLegacy, backupDir))
|
||||||
|
}
|
||||||
|
|
||||||
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
@@ -573,29 +605,118 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupHyprlandConfigFile(src, dst string, data []byte, removeSource bool) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removeSource {
|
||||||
|
if err := os.Remove(src); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, error) {
|
||||||
|
legacyPaths := []string{filepath.Join(configDir, "hyprland.conf")}
|
||||||
|
dmsConfPaths, err := filepath.Glob(filepath.Join(dmsDir, "*.conf"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
legacyPaths = append(legacyPaths, dmsConfPaths...)
|
||||||
|
backupPaths, err := adjacentHyprlandBackupFiles(configDir, dmsDir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
legacyPaths = append(legacyPaths, backupPaths...)
|
||||||
|
|
||||||
|
moved := 0
|
||||||
|
for _, src := range legacyPaths {
|
||||||
|
info, err := os.Lstat(src)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return moved, err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(configDir, src)
|
||||||
|
if err != nil {
|
||||||
|
rel = filepath.Base(src)
|
||||||
|
}
|
||||||
|
dst := filepath.Join(backupDir, rel)
|
||||||
|
if err := moveHyprlandConfigFile(src, dst); err != nil {
|
||||||
|
return moved, err
|
||||||
|
}
|
||||||
|
moved++
|
||||||
|
}
|
||||||
|
|
||||||
|
return moved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveHyprlandConfigFile(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjacentHyprlandBackupFiles(configDir, dmsDir string) ([]string, error) {
|
||||||
|
var paths []string
|
||||||
|
patterns := []string{
|
||||||
|
filepath.Join(configDir, "hyprland.conf.backup.*"),
|
||||||
|
filepath.Join(configDir, "hyprland.lua.backup.*"),
|
||||||
|
filepath.Join(dmsDir, "*.conf.backup.*"),
|
||||||
|
filepath.Join(dmsDir, "*.lua.backup.*"),
|
||||||
|
}
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
paths = append(paths, matches...)
|
||||||
|
}
|
||||||
|
return paths, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||||
configs := []struct {
|
configs := []struct {
|
||||||
name string
|
name string
|
||||||
content string
|
content string
|
||||||
|
overwrite bool
|
||||||
}{
|
}{
|
||||||
{"colors.conf", HyprColorsConfig},
|
{name: "colors.lua", content: DMSColorsLuaConfig},
|
||||||
{"layout.conf", HyprLayoutConfig},
|
{name: "layout.lua", content: DMSLayoutLuaConfig},
|
||||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{name: "binds.lua", content: strings.ReplaceAll(DMSBindsLuaConfig, "{{TERMINAL_COMMAND}}", terminalCommand), overwrite: true},
|
||||||
{"outputs.conf", ""},
|
{name: "binds-user.lua", content: DMSBindsUserLuaConfig},
|
||||||
{"cursor.conf", ""},
|
{name: "outputs.lua", content: DMSOutputsLuaConfig},
|
||||||
{"windowrules.conf", ""},
|
{name: "cursor.lua", content: DMSCursorLuaConfig},
|
||||||
|
{name: "windowrules.lua", content: DMSWindowRulesLuaConfig},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
// Skip if file already exists and is not empty to preserve user modifications
|
existed := false
|
||||||
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
existed = true
|
||||||
|
}
|
||||||
|
if existed && !cfg.overwrite {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
|
if existed {
|
||||||
|
cd.log(fmt.Sprintf("Updated %s", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,94 +724,42 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
_ = newConfig
|
||||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
lines := extractHyprlangMonitorLines(existingConfig)
|
||||||
|
if len(lines) == 0 {
|
||||||
if len(existingMonitors) == 0 {
|
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
outputsPath := filepath.Join(dmsDir, "outputs.lua")
|
||||||
if _, err := os.Stat(outputsPath); err != nil {
|
if info, err := os.Stat(outputsPath); err == nil && info.Size() > 0 {
|
||||||
var outputsContent strings.Builder
|
cd.log("Skipping monitor migration: dms/outputs.lua already exists")
|
||||||
for _, monitor := range existingMonitors {
|
return newConfig, nil
|
||||||
outputsContent.WriteString(monitor)
|
|
||||||
outputsContent.WriteString("\n")
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
|
||||||
} else {
|
|
||||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
var b strings.Builder
|
||||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
b.WriteString("-- Migrated from existing hyprlang monitor lines\n\n")
|
||||||
|
ok := 0
|
||||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
|
||||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
|
||||||
|
|
||||||
if headerMatch == nil {
|
|
||||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
|
||||||
}
|
|
||||||
|
|
||||||
insertPos := headerMatch[1] + 1
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.WriteString(mergedConfig[:insertPos])
|
|
||||||
builder.WriteString("# Monitors from existing configuration\n")
|
|
||||||
|
|
||||||
for _, monitor := range existingMonitors {
|
|
||||||
builder.WriteString(monitor)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(mergedConfig[insertPos:])
|
|
||||||
|
|
||||||
return builder.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
|
|
||||||
lines := strings.Split(config, "\n")
|
|
||||||
var result []string
|
|
||||||
startupSectionFound := false
|
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
lua, err := hyprlangMonitorLineToLua(line)
|
||||||
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
|
if err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: could not migrate monitor line %q: %v", line, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
b.WriteString(lua)
|
||||||
startupSectionFound = true
|
b.WriteByte('\n')
|
||||||
result = append(result, "exec-once = dms run")
|
ok++
|
||||||
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
|
|
||||||
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
|
||||||
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, line)
|
|
||||||
}
|
}
|
||||||
|
if ok == 0 {
|
||||||
if !startupSectionFound {
|
return newConfig, nil
|
||||||
for i, line := range result {
|
|
||||||
if strings.Contains(line, "STARTUP APPS") {
|
|
||||||
insertLines := []string{
|
|
||||||
"exec-once = dms run",
|
|
||||||
"env = QT_QPA_PLATFORM,wayland;xcb",
|
|
||||||
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
|
||||||
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
|
||||||
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
|
||||||
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
|
|
||||||
}
|
|
||||||
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
b.WriteByte('\n')
|
||||||
return strings.Join(result, "\n")
|
b.WriteString("-- Default fallback\n")
|
||||||
|
b.WriteString("hl.monitor({ output = \"\", mode = \"preferred\", position = \"auto\", scale = \"auto\" })\n")
|
||||||
|
if err := os.WriteFile(outputsPath, []byte(b.String()), 0o644); err != nil {
|
||||||
|
return newConfig, err
|
||||||
|
}
|
||||||
|
cd.log("Migrated monitor sections to dms/outputs.lua")
|
||||||
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
|||||||
@@ -11,6 +11,46 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCleanupStrayHyprlandConfFile(t *testing.T) {
|
||||||
|
if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") == "" {
|
||||||
|
t.Setenv("HYPRLAND_INSTANCE_SIGNATURE", "test-signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("leaves conf alone when no hyprland.lua present", func(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
t.Setenv("HOME", td)
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
require.NoError(t, os.WriteFile(confPath, []byte("# legacy user config\n"), 0o644))
|
||||||
|
|
||||||
|
CleanupStrayHyprlandConfFile(nil)
|
||||||
|
|
||||||
|
assert.FileExists(t, confPath, "must not touch hyprland.conf when user has not migrated")
|
||||||
|
assert.NoDirExists(t, filepath.Join(configDir, hyprlandBackupDirName))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("moves stray conf into backup when hyprland.lua exists", func(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
t.Setenv("HOME", td)
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
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")
|
||||||
|
require.NoError(t, os.WriteFile(confPath, []byte("# autogen\n"), 0o644))
|
||||||
|
|
||||||
|
CleanupStrayHyprlandConfFile(nil)
|
||||||
|
|
||||||
|
assert.NoFileExists(t, confPath)
|
||||||
|
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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMergeNiriOutputSections(t *testing.T) {
|
func TestMergeNiriOutputSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
@@ -259,130 +299,56 @@ func getGhosttyPath() string {
|
|||||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
tests := []struct {
|
t.Run("no monitors in existing", func(t *testing.T) {
|
||||||
name string
|
tmp := t.TempDir()
|
||||||
newConfig string
|
out, err := cd.mergeHyprlandMonitorSections(`hl.config({})`, `input { kb_layout = us }`, tmp)
|
||||||
existingConfig string
|
require.NoError(t, err)
|
||||||
wantError bool
|
assert.Equal(t, `hl.config({})`, out)
|
||||||
wantContains []string
|
_, e := os.Stat(filepath.Join(tmp, "outputs.lua"))
|
||||||
wantNotContains []string
|
assert.True(t, os.IsNotExist(e))
|
||||||
}{
|
})
|
||||||
{
|
|
||||||
name: "no existing monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
t.Run("writes outputs lua from hyprlang monitors", func(t *testing.T) {
|
||||||
# ENVIRONMENT VARS
|
tmp := t.TempDir()
|
||||||
# ==================
|
existing := `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
env = XDG_CURRENT_DESKTOP,niri`,
|
|
||||||
existingConfig: `# Some other config
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
wantError: false,
|
|
||||||
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "merge single monitor",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================`,
|
|
||||||
existingConfig: `# My config
|
|
||||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
wantError: false,
|
|
||||||
wantContains: []string{
|
|
||||||
"MONITOR CONFIG",
|
|
||||||
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
|
||||||
"Monitors from existing configuration",
|
|
||||||
},
|
|
||||||
wantNotContains: []string{
|
|
||||||
"monitor = eDP-2", // Example monitor should be removed
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "merge multiple monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ENVIRONMENT VARS
|
|
||||||
# ==================`,
|
|
||||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
|
||||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
monitor = eDP-1, 2560x1440@165, auto, 1.25`
|
||||||
wantError: false,
|
out, err := cd.mergeHyprlandMonitorSections(`return`, existing, tmp)
|
||||||
wantContains: []string{
|
require.NoError(t, err)
|
||||||
"monitor = DP-1",
|
assert.Equal(t, `return`, out)
|
||||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
b, err := os.ReadFile(filepath.Join(tmp, "outputs.lua"))
|
||||||
"monitor = eDP-1",
|
require.NoError(t, err)
|
||||||
"Monitors from existing configuration",
|
s := string(b)
|
||||||
},
|
assert.Contains(t, s, "hl.monitor")
|
||||||
wantNotContains: []string{
|
assert.Contains(t, s, "DP-1")
|
||||||
"monitor = eDP-2", // Example monitor should be removed
|
assert.Contains(t, s, "HDMI-A-1")
|
||||||
},
|
assert.Contains(t, s, "eDP-1")
|
||||||
},
|
assert.Contains(t, s, "preferred") // fallback rule at end
|
||||||
{
|
})
|
||||||
name: "preserve commented monitors",
|
|
||||||
newConfig: `# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
|
|
||||||
# ==================`,
|
t.Run("skips when outputs lua already exists", func(t *testing.T) {
|
||||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
tmp := t.TempDir()
|
||||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
path := filepath.Join(tmp, "outputs.lua")
|
||||||
wantError: false,
|
require.NoError(t, os.WriteFile(path, []byte("-- keep\n"), 0o644))
|
||||||
wantContains: []string{
|
_, err := cd.mergeHyprlandMonitorSections(`x`, `monitor = DP-1, 1920x1080@144, 0x0, 1`, tmp)
|
||||||
"# monitor = DP-1",
|
require.NoError(t, err)
|
||||||
"# monitor = HDMI-A-1",
|
b, err := os.ReadFile(path)
|
||||||
"Monitors from existing configuration",
|
require.NoError(t, err)
|
||||||
},
|
assert.Equal(t, "-- keep\n", string(b))
|
||||||
},
|
})
|
||||||
{
|
}
|
||||||
name: "no monitor config section",
|
|
||||||
newConfig: `# Some config without monitor section
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
}`,
|
|
||||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
|
||||||
wantError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
func TestHyprlangMonitorLineToLuaPreservesOptions(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
got, err := hyprlangMonitorLineToLua(`monitor = DP-1, 1920x1080@144, 0x0, 1, transform, 1, vrr, 2, bitdepth, 10, cm, hdr, sdrbrightness, 1.2, sdrsaturation, 0.98`)
|
||||||
tmpDir := t.TempDir()
|
require.NoError(t, err)
|
||||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
|
||||||
|
|
||||||
if tt.wantError {
|
assert.Contains(t, got, `output = "DP-1"`)
|
||||||
assert.Error(t, err)
|
assert.Contains(t, got, `transform = 1`)
|
||||||
return
|
assert.Contains(t, got, `vrr = 2`)
|
||||||
}
|
assert.Contains(t, got, `bitdepth = 10`)
|
||||||
|
assert.Contains(t, got, `cm = "hdr"`)
|
||||||
require.NoError(t, err)
|
assert.Contains(t, got, `sdrbrightness = 1.2`)
|
||||||
|
assert.Contains(t, got, `sdrsaturation = 0.98`)
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, notWant := range tt.wantNotContains {
|
|
||||||
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||||
@@ -398,6 +364,10 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
cd := NewConfigDeployer(logChan)
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-empty")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -408,12 +378,16 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
|
|
||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), `require("dms.binds")`)
|
||||||
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
assert.Contains(t, string(content), "DMS_STARTUP_BEGIN")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "hl.config(")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-merge")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
existingContent := `# My existing Hyprland config
|
existingContent := `# My existing Hyprland config
|
||||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||||
@@ -422,11 +396,17 @@ general {
|
|||||||
gaps_in = 10
|
gaps_in = 10
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
hyprPath := filepath.Join(td, ".config", "hypr", "hyprland.conf")
|
||||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
err = os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
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, "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))
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -440,13 +420,76 @@ general {
|
|||||||
backupContent, err := os.ReadFile(result.BackupPath)
|
backupContent, err := os.ReadFile(result.BackupPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, existingContent, string(backupContent))
|
assert.Equal(t, existingContent, string(backupContent))
|
||||||
|
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", "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, "cursor.conf"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(filepath.Dir(hyprPath), "hyprland.conf.backup.old"))
|
||||||
|
assert.NoFileExists(t, filepath.Join(dmsDir, "binds.conf.backup.old"))
|
||||||
|
|
||||||
newContent, err := os.ReadFile(result.Path)
|
newContent, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
assert.Contains(t, string(newContent), `require("dms.binds")`)
|
||||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
|
||||||
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
outputsPath := filepath.Join(td, ".config", "hypr", "dms", "outputs.lua")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
outBytes, err := os.ReadFile(outputsPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
outs := string(outBytes)
|
||||||
|
assert.Contains(t, outs, `hl.monitor`)
|
||||||
|
assert.Contains(t, outs, "DP-1")
|
||||||
|
assert.Contains(t, outs, "HDMI-A-1")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config removes root legacy symlink when lua exists", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-lua-conf-symlink")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
|
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
require.NoError(t, os.WriteFile(luaPath, []byte(`require("dms.binds")`+"\n"), 0o644))
|
||||||
|
require.NoError(t, os.Symlink(filepath.Join(configDir, "missing-legacy.conf"), confPath))
|
||||||
|
|
||||||
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, luaPath, result.Path)
|
||||||
|
_, err = os.Lstat(confPath)
|
||||||
|
assert.True(t, os.IsNotExist(err), "root hyprland.conf symlink should be moved out of the live config directory")
|
||||||
|
_, err = os.Lstat(filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deploy hyprland config refreshes managed binds but preserves user binds", func(t *testing.T) {
|
||||||
|
td, err := os.MkdirTemp("", "dankinstall-hyprland-refresh-binds")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
os.Setenv("HOME", td)
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(td, ".config", "hypr", "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte("-- stale managed binds\n"), 0o644))
|
||||||
|
userBinds := "-- custom user binds\n"
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(userBinds), 0o644))
|
||||||
|
|
||||||
|
_, err = cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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.exec_cmd([[hyprctl dispatch resizeactive -10% 0]]), { repeating = true })`)
|
||||||
|
|
||||||
|
user, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, userBinds, string(user))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,10 +502,10 @@ func TestNiriConfigStructure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHyprlandConfigStructure(t *testing.T) {
|
func TestHyprlandConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandLuaConfig, `require("dms.binds")`)
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandLuaConfig, "DMS_STARTUP_BEGIN")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandLuaConfig, "hl.config(")
|
||||||
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
@@ -789,4 +832,37 @@ func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("hyprland legacy config exists skips when replace false", func(t *testing.T) {
|
||||||
|
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-legacy-skip-test")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
originalHome := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tempDir)
|
||||||
|
defer os.Setenv("HOME", originalHome)
|
||||||
|
|
||||||
|
hyprConf := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(hyprConf), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(hyprConf, []byte("monitor = , preferred, auto, 1\n"), 0o644))
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
results, err := cd.deployConfigurationsInternal(
|
||||||
|
context.Background(),
|
||||||
|
deps.WindowManagerHyprland,
|
||||||
|
deps.TerminalGhostty,
|
||||||
|
nil,
|
||||||
|
allFalse,
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, r := range results {
|
||||||
|
if r.ConfigType == "Hyprland" && r.Deployed {
|
||||||
|
t.Fatalf("expected Hyprland deployment to be skipped when legacy config exists and replace=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Optional per-user keybind overrides (managed by DMS). Loaded after default binds.
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# === Application Launchers ===
|
|
||||||
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
|
|
||||||
bind = SUPER, space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
|
|
||||||
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
|
|
||||||
bind = SUPER, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
|
|
||||||
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
|
||||||
|
|
||||||
# === Cheat sheet
|
|
||||||
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
bind = SUPER ALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = SUPER SHIFT, E, exit
|
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
|
||||||
|
|
||||||
# === Audio Controls ===
|
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|
||||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
|
||||||
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
|
||||||
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
|
||||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
|
||||||
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
|
|
||||||
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
|
|
||||||
|
|
||||||
# === Brightness Controls ===
|
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# === Window Management ===
|
|
||||||
bind = SUPER, Q, killactive
|
|
||||||
bind = SUPER, F, fullscreen, 1
|
|
||||||
bind = SUPER SHIFT, F, fullscreen, 0
|
|
||||||
bind = SUPER SHIFT, T, togglefloating
|
|
||||||
bind = SUPER, W, togglegroup
|
|
||||||
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
|
||||||
|
|
||||||
# === Focus Navigation ===
|
|
||||||
bind = SUPER, left, movefocus, l
|
|
||||||
bind = SUPER, down, movefocus, d
|
|
||||||
bind = SUPER, up, movefocus, u
|
|
||||||
bind = SUPER, right, movefocus, r
|
|
||||||
bind = SUPER, H, movefocus, l
|
|
||||||
bind = SUPER, J, movefocus, d
|
|
||||||
bind = SUPER, K, movefocus, u
|
|
||||||
bind = SUPER, L, movefocus, r
|
|
||||||
|
|
||||||
# === Window Movement ===
|
|
||||||
bind = SUPER SHIFT, left, movewindow, l
|
|
||||||
bind = SUPER SHIFT, down, movewindow, d
|
|
||||||
bind = SUPER SHIFT, up, movewindow, u
|
|
||||||
bind = SUPER SHIFT, right, movewindow, r
|
|
||||||
bind = SUPER SHIFT, H, movewindow, l
|
|
||||||
bind = SUPER SHIFT, J, movewindow, d
|
|
||||||
bind = SUPER SHIFT, K, movewindow, u
|
|
||||||
bind = SUPER SHIFT, L, movewindow, r
|
|
||||||
|
|
||||||
# === Column Navigation ===
|
|
||||||
bind = SUPER, Home, focuswindow, first
|
|
||||||
bind = SUPER, End, focuswindow, last
|
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
|
||||||
bind = SUPER CTRL, left, focusmonitor, l
|
|
||||||
bind = SUPER CTRL, right, focusmonitor, r
|
|
||||||
bind = SUPER CTRL, H, focusmonitor, l
|
|
||||||
bind = SUPER CTRL, J, focusmonitor, d
|
|
||||||
bind = SUPER CTRL, K, focusmonitor, u
|
|
||||||
bind = SUPER CTRL, L, focusmonitor, r
|
|
||||||
|
|
||||||
# === Move to Monitor ===
|
|
||||||
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
|
|
||||||
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
|
|
||||||
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
|
|
||||||
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
|
|
||||||
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
|
|
||||||
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
|
|
||||||
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
|
|
||||||
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
|
|
||||||
|
|
||||||
# === Workspace Navigation ===
|
|
||||||
bind = SUPER, Page_Down, workspace, e+1
|
|
||||||
bind = SUPER, Page_Up, workspace, e-1
|
|
||||||
bind = SUPER, U, workspace, e+1
|
|
||||||
bind = SUPER, I, workspace, e-1
|
|
||||||
bind = SUPER CTRL, down, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, up, movetoworkspace, e-1
|
|
||||||
bind = SUPER CTRL, U, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Workspace Management ===
|
|
||||||
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
|
||||||
|
|
||||||
# === Move Workspaces ===
|
|
||||||
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
|
||||||
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
|
||||||
bind = SUPER SHIFT, U, movetoworkspace, e+1
|
|
||||||
bind = SUPER SHIFT, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Mouse Wheel Navigation ===
|
|
||||||
bind = SUPER, mouse_down, workspace, e+1
|
|
||||||
bind = SUPER, mouse_up, workspace, e-1
|
|
||||||
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
|
|
||||||
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Numbered Workspaces ===
|
|
||||||
bind = SUPER, 1, workspace, 1
|
|
||||||
bind = SUPER, 2, workspace, 2
|
|
||||||
bind = SUPER, 3, workspace, 3
|
|
||||||
bind = SUPER, 4, workspace, 4
|
|
||||||
bind = SUPER, 5, workspace, 5
|
|
||||||
bind = SUPER, 6, workspace, 6
|
|
||||||
bind = SUPER, 7, workspace, 7
|
|
||||||
bind = SUPER, 8, workspace, 8
|
|
||||||
bind = SUPER, 9, workspace, 9
|
|
||||||
|
|
||||||
# === Move to Numbered Workspaces ===
|
|
||||||
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
|
||||||
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
|
||||||
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
|
||||||
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
|
||||||
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
|
||||||
bind = SUPER SHIFT, 6, movetoworkspace, 6
|
|
||||||
bind = SUPER SHIFT, 7, movetoworkspace, 7
|
|
||||||
bind = SUPER SHIFT, 8, movetoworkspace, 8
|
|
||||||
bind = SUPER SHIFT, 9, movetoworkspace, 9
|
|
||||||
|
|
||||||
# === Column Management ===
|
|
||||||
bind = SUPER, bracketleft, layoutmsg, preselect l
|
|
||||||
bind = SUPER, bracketright, layoutmsg, preselect r
|
|
||||||
|
|
||||||
# === Sizing & Layout ===
|
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
|
||||||
bindmd = SUPER, mouse:273, Resize window, resizewindow
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
|
|
||||||
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
|
|
||||||
|
|
||||||
# === Manual Sizing ===
|
|
||||||
binde = SUPER, minus, resizeactive, -10% 0
|
|
||||||
binde = SUPER, equal, resizeactive, 10% 0
|
|
||||||
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
|
|
||||||
binde = SUPER SHIFT, equal, resizeactive, 0 10%
|
|
||||||
|
|
||||||
# === Screenshots ===
|
|
||||||
bind = , Print, exec, dms screenshot
|
|
||||||
bind = CTRL, Print, exec, dms screenshot full
|
|
||||||
bind = ALT, Print, exec, dms screenshot window
|
|
||||||
|
|
||||||
# === Display Profiles ===
|
|
||||||
bind = SUPER, P, exec, dms ipc outputs cycleProfile
|
|
||||||
|
|
||||||
# === System Controls ===
|
|
||||||
bind = SUPER SHIFT, P, dpms, toggle
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
-- DMS default keybinds (Hyprland 0.55+ Lua)
|
||||||
|
|
||||||
|
-- === Application Launchers ===
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("{{TERMINAL_COMMAND}}"))
|
||||||
|
hl.bind("SUPER + space", hl.dsp.exec_cmd("dms ipc call spotlight toggle"))
|
||||||
|
hl.bind("ALT + space", hl.dsp.exec_cmd("dms ipc call spotlight-bar toggle"))
|
||||||
|
hl.bind("SUPER + V", hl.dsp.exec_cmd("dms ipc call clipboard toggle"))
|
||||||
|
hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
||||||
|
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||||
|
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
||||||
|
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
|
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||||
|
|
||||||
|
-- === Cheat sheet
|
||||||
|
hl.bind("SUPER + SHIFT + Slash", hl.dsp.exec_cmd("dms ipc call keybinds toggle hyprland"))
|
||||||
|
|
||||||
|
-- === Security ===
|
||||||
|
hl.bind("SUPER + ALT + L", hl.dsp.exec_cmd("dms ipc call lock lock"))
|
||||||
|
hl.bind("SUPER + SHIFT + E", hl.dsp.exit())
|
||||||
|
hl.bind("CTRL + ALT + Delete", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
||||||
|
|
||||||
|
-- === Audio Controls ===
|
||||||
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("dms ipc call audio increment 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("dms ipc call audio decrement 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("dms ipc call audio mute"), { locked = true })
|
||||||
|
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("dms ipc call audio micmute"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("dms ipc call mpris playPause"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("dms ipc call mpris playPause"), { locked = true })
|
||||||
|
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("dms ipc call mpris previous"), { locked = true })
|
||||||
|
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("dms ipc call mpris next"), { locked = true })
|
||||||
|
hl.bind("CTRL + XF86AudioRaiseVolume", hl.dsp.exec_cmd("dms ipc call mpris increment 3"), { locked = true, repeating = true })
|
||||||
|
hl.bind("CTRL + XF86AudioLowerVolume", hl.dsp.exec_cmd("dms ipc call mpris decrement 3"), { locked = true, repeating = true })
|
||||||
|
|
||||||
|
-- === Brightness Controls ===
|
||||||
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]]), { locked = true, repeating = true })
|
||||||
|
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.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" }))
|
||||||
|
hl.bind("SUPER + W", hl.dsp.group.toggle())
|
||||||
|
hl.bind("SUPER + SHIFT + W", hl.dsp.exec_cmd("dms ipc call window-rules toggle"))
|
||||||
|
|
||||||
|
-- === Focus Navigation ===
|
||||||
|
hl.bind("SUPER + left", hl.dsp.focus({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + down", hl.dsp.focus({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + right", hl.dsp.focus({ direction = "r" }))
|
||||||
|
hl.bind("SUPER + H", hl.dsp.focus({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + J", hl.dsp.focus({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + K", hl.dsp.focus({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + L", hl.dsp.focus({ direction = "r" }))
|
||||||
|
|
||||||
|
-- === Window Movement ===
|
||||||
|
hl.bind("SUPER + SHIFT + left", hl.dsp.window.move({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + down", hl.dsp.window.move({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + up", hl.dsp.window.move({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + right", hl.dsp.window.move({ direction = "r" }))
|
||||||
|
hl.bind("SUPER + SHIFT + H", hl.dsp.window.move({ direction = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + J", hl.dsp.window.move({ direction = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + K", hl.dsp.window.move({ direction = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + L", hl.dsp.window.move({ direction = "r" }))
|
||||||
|
|
||||||
|
-- === Column Navigation ===
|
||||||
|
hl.bind("SUPER + Home", hl.dsp.focus({ window = "first" }))
|
||||||
|
hl.bind("SUPER + End", hl.dsp.focus({ window = "last" }))
|
||||||
|
|
||||||
|
-- === Monitor Navigation ===
|
||||||
|
hl.bind("SUPER + CTRL + left", hl.dsp.focus({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + CTRL + right", hl.dsp.focus({ monitor = "r" }))
|
||||||
|
hl.bind("SUPER + CTRL + H", hl.dsp.focus({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + CTRL + J", hl.dsp.focus({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + CTRL + K", hl.dsp.focus({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + CTRL + L", hl.dsp.focus({ monitor = "r" }))
|
||||||
|
|
||||||
|
-- === Move to Monitor ===
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + left", hl.dsp.window.move({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + down", hl.dsp.window.move({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + up", hl.dsp.window.move({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + right", hl.dsp.window.move({ monitor = "r" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + H", hl.dsp.window.move({ monitor = "l" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + J", hl.dsp.window.move({ monitor = "d" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + K", hl.dsp.window.move({ monitor = "u" }))
|
||||||
|
hl.bind("SUPER + SHIFT + CTRL + L", hl.dsp.window.move({ monitor = "r" }))
|
||||||
|
|
||||||
|
-- === Workspace Navigation ===
|
||||||
|
hl.bind("SUPER + Page_Down", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + Page_Up", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + U", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + I", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + CTRL + down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + CTRL + up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + CTRL + U", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + CTRL + I", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Workspace Management ===
|
||||||
|
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd("dms ipc call workspace-rename open"))
|
||||||
|
|
||||||
|
-- === Move Workspaces ===
|
||||||
|
hl.bind("SUPER + SHIFT + Page_Down", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + Page_Up", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + U", hl.dsp.window.move({ workspace = "e+1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + I", hl.dsp.window.move({ workspace = "e-1" }))
|
||||||
|
|
||||||
|
-- === Mouse Wheel Navigation ===
|
||||||
|
hl.bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||||
|
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" }))
|
||||||
|
|
||||||
|
-- === Numbered Workspaces ===
|
||||||
|
hl.bind("SUPER + 1", hl.dsp.focus({ workspace = "1" }))
|
||||||
|
hl.bind("SUPER + 2", hl.dsp.focus({ workspace = "2" }))
|
||||||
|
hl.bind("SUPER + 3", hl.dsp.focus({ workspace = "3" }))
|
||||||
|
hl.bind("SUPER + 4", hl.dsp.focus({ workspace = "4" }))
|
||||||
|
hl.bind("SUPER + 5", hl.dsp.focus({ workspace = "5" }))
|
||||||
|
hl.bind("SUPER + 6", hl.dsp.focus({ workspace = "6" }))
|
||||||
|
hl.bind("SUPER + 7", hl.dsp.focus({ workspace = "7" }))
|
||||||
|
hl.bind("SUPER + 8", hl.dsp.focus({ workspace = "8" }))
|
||||||
|
hl.bind("SUPER + 9", hl.dsp.focus({ workspace = "9" }))
|
||||||
|
|
||||||
|
-- === Move to Numbered Workspaces ===
|
||||||
|
hl.bind("SUPER + SHIFT + 1", hl.dsp.window.move({ workspace = "1" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 2", hl.dsp.window.move({ workspace = "2" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 3", hl.dsp.window.move({ workspace = "3" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 4", hl.dsp.window.move({ workspace = "4" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 5", hl.dsp.window.move({ workspace = "5" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 6", hl.dsp.window.move({ workspace = "6" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 7", hl.dsp.window.move({ workspace = "7" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 8", hl.dsp.window.move({ workspace = "8" }))
|
||||||
|
hl.bind("SUPER + SHIFT + 9", hl.dsp.window.move({ workspace = "9" }))
|
||||||
|
|
||||||
|
-- === Column Management ===
|
||||||
|
hl.bind("SUPER + bracketleft", hl.dsp.layout("preselect l"))
|
||||||
|
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.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" })
|
||||||
|
hl.bind("SUPER + mouse:273", hl.dsp.window.resize(), { mouse = true, description = "Resize window" })
|
||||||
|
|
||||||
|
hl.bind("SUPER + code:20", hl.dsp.window.resize({ x = -100, y = 0, relative = true }), { description = "Expand window left" })
|
||||||
|
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.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"))
|
||||||
|
hl.bind("CTRL + Print", hl.dsp.exec_cmd("dms screenshot full"))
|
||||||
|
hl.bind("ALT + Print", hl.dsp.exec_cmd("dms screenshot window"))
|
||||||
|
|
||||||
|
-- === Display Profiles ===
|
||||||
|
hl.bind("SUPER + P", hl.dsp.exec_cmd("dms ipc outputs cycleProfile"))
|
||||||
|
|
||||||
|
-- === System Controls ===
|
||||||
|
hl.bind("SUPER + SHIFT + P", hl.dsp.dpms({ action = "toggle" }))
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# ! Auto-generated file. Do not edit directly.
|
|
||||||
# Remove source = ./dms/colors.conf from your config to override.
|
|
||||||
|
|
||||||
$primary = rgb(d0bcff)
|
|
||||||
$outline = rgb(948f99)
|
|
||||||
$error = rgb(f2b8b5)
|
|
||||||
|
|
||||||
general {
|
|
||||||
col.active_border = $primary
|
|
||||||
col.inactive_border = $outline
|
|
||||||
}
|
|
||||||
|
|
||||||
group {
|
|
||||||
col.border_active = $primary
|
|
||||||
col.border_inactive = $outline
|
|
||||||
col.border_locked_active = $error
|
|
||||||
col.border_locked_inactive = $outline
|
|
||||||
|
|
||||||
groupbar {
|
|
||||||
col.active = $primary
|
|
||||||
col.inactive = $outline
|
|
||||||
col.locked_active = $error
|
|
||||||
col.locked_inactive = $outline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- ! Auto-generated file. Do not edit directly.
|
||||||
|
-- Regenerate via DMS theme tools or remove require("dms.colors") from hyprland.lua to override.
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
col = {
|
||||||
|
active_border = "rgb(d0bcff)",
|
||||||
|
inactive_border = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group = {
|
||||||
|
col = {
|
||||||
|
border_active = "rgb(d0bcff)",
|
||||||
|
border_inactive = "rgb(948f99)",
|
||||||
|
border_locked_active = "rgb(f2b8b5)",
|
||||||
|
border_locked_inactive = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
groupbar = {
|
||||||
|
col = {
|
||||||
|
active = "rgb(d0bcff)",
|
||||||
|
inactive = "rgb(948f99)",
|
||||||
|
locked_active = "rgb(f2b8b5)",
|
||||||
|
locked_inactive = "rgb(948f99)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Cursor theme overrides. Deploy writes ~/.config/hypr/dms/cursor.lua
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Auto-generated by DMS - do not edit manually
|
|
||||||
|
|
||||||
general {
|
|
||||||
gaps_in = 4
|
|
||||||
gaps_out = 4
|
|
||||||
border_size = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
decoration {
|
|
||||||
rounding = 12
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Auto-generated by DMS — do not edit manually
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
gaps_in = 4,
|
||||||
|
gaps_out = 4,
|
||||||
|
border_size = 2,
|
||||||
|
},
|
||||||
|
decoration = {
|
||||||
|
rounding = 12,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Per-output monitor rules — embedded sibling of the legacy outputs.conf fragment. Deploy writes ~/.config/hypr/dms/outputs.lua
|
||||||
|
|
||||||
|
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- Window rules. Deploy writes ~/.config/hypr/dms/windowrules.lua
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# Hyprland Configuration
|
|
||||||
# https://wiki.hypr.land/Configuring/
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# MONITOR CONFIG
|
|
||||||
# ==================
|
|
||||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
|
||||||
monitor = , preferred,auto,auto
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# STARTUP APPS
|
|
||||||
# ==================
|
|
||||||
exec-once = dbus-update-activation-environment --systemd --all
|
|
||||||
exec-once = systemctl --user start hyprland-session.target
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# INPUT CONFIG
|
|
||||||
# ==================
|
|
||||||
input {
|
|
||||||
kb_layout = us
|
|
||||||
numlock_by_default = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# GENERAL LAYOUT
|
|
||||||
# ==================
|
|
||||||
general {
|
|
||||||
gaps_in = 5
|
|
||||||
gaps_out = 5
|
|
||||||
border_size = 2
|
|
||||||
|
|
||||||
layout = dwindle
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# DECORATION
|
|
||||||
# ==================
|
|
||||||
decoration {
|
|
||||||
rounding = 12
|
|
||||||
|
|
||||||
active_opacity = 1.0
|
|
||||||
inactive_opacity = 1.0
|
|
||||||
|
|
||||||
shadow {
|
|
||||||
enabled = true
|
|
||||||
range = 30
|
|
||||||
render_power = 5
|
|
||||||
offset = 0 5
|
|
||||||
color = rgba(00000070)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# ANIMATIONS
|
|
||||||
# ==================
|
|
||||||
animations {
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
animation = windowsIn, 1, 3, default
|
|
||||||
animation = windowsOut, 1, 3, default
|
|
||||||
animation = workspaces, 1, 5, default
|
|
||||||
animation = windowsMove, 1, 4, default
|
|
||||||
animation = fade, 1, 3, default
|
|
||||||
animation = border, 1, 3, default
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# LAYOUTS
|
|
||||||
# ==================
|
|
||||||
dwindle {
|
|
||||||
preserve_split = true
|
|
||||||
}
|
|
||||||
|
|
||||||
master {
|
|
||||||
mfact = 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# MISC
|
|
||||||
# ==================
|
|
||||||
misc {
|
|
||||||
disable_hyprland_logo = true
|
|
||||||
disable_splash_rendering = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# WINDOW RULES
|
|
||||||
# ==================
|
|
||||||
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
|
||||||
|
|
||||||
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
|
||||||
|
|
||||||
windowrule = tile on, match:class ^(gnome-control-center)$
|
|
||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
|
||||||
|
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
|
||||||
windowrule = float on, match:class ^(galculator)$
|
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
|
||||||
|
|
||||||
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
|
||||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
|
||||||
|
|
||||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
|
||||||
windowrule = float on, match:class ^(zoom)$
|
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
|
||||||
layerrule = no_anim on, match:namespace ^dms:.*
|
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
|
||||||
source = ./dms/outputs.conf
|
|
||||||
source = ./dms/layout.conf
|
|
||||||
source = ./dms/cursor.conf
|
|
||||||
source = ./dms/binds.conf
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
-- Hyprland configuration (Lua) — https://wiki.hypr.land/Configuring/Start/
|
||||||
|
|
||||||
|
hl.config({ autogenerated = false })
|
||||||
|
|
||||||
|
-- DMS_STARTUP_BEGIN
|
||||||
|
hl.on("hyprland.start", function()
|
||||||
|
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
|
||||||
|
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||||
|
end)
|
||||||
|
-- DMS_STARTUP_END
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
input = {
|
||||||
|
kb_layout = "us",
|
||||||
|
numlock_by_default = true,
|
||||||
|
},
|
||||||
|
general = {
|
||||||
|
gaps_in = 5,
|
||||||
|
gaps_out = 5,
|
||||||
|
border_size = 2,
|
||||||
|
layout = "dwindle",
|
||||||
|
},
|
||||||
|
decoration = {
|
||||||
|
rounding = 12,
|
||||||
|
active_opacity = 1.0,
|
||||||
|
inactive_opacity = 1.0,
|
||||||
|
shadow = {
|
||||||
|
enabled = true,
|
||||||
|
range = 30,
|
||||||
|
render_power = 5,
|
||||||
|
offset = "0 5",
|
||||||
|
color = "rgba(00000070)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
misc = {
|
||||||
|
disable_hyprland_logo = true,
|
||||||
|
disable_splash_rendering = true,
|
||||||
|
},
|
||||||
|
dwindle = {
|
||||||
|
preserve_split = true,
|
||||||
|
},
|
||||||
|
master = {
|
||||||
|
mfact = 0.5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.animation({ leaf = "windowsIn", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "windowsMove", enabled = true, speed = 4, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "fade", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "border", enabled = true, speed = 3, bezier = "default" })
|
||||||
|
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.wezfurlong\\.wezterm)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.)" }, rounding = 12 })
|
||||||
|
hl.window_rule({ match = { class = "^(gnome-control-center)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(pavucontrol)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(nm-connection-editor)$" }, tile = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.Calculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(gnome-calculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(galculator)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(blueman-manager)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(org\\.gnome\\.Nautilus)$" }, float = true })
|
||||||
|
hl.window_rule({ match = { class = "^(xdg-desktop-portal)$" }, float = true })
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(steam)$", title = "^(notificationtoasts)" },
|
||||||
|
no_initial_focus = true,
|
||||||
|
pin = true,
|
||||||
|
})
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(firefox)$", title = "^(Picture-in-Picture)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
hl.window_rule({ match = { class = "^(zoom)$" }, float = true })
|
||||||
|
hl.layer_rule({ match = { namespace = "^(quickshell)$" }, no_anim = true })
|
||||||
|
hl.layer_rule({ match = { namespace = "^dms:.*" }, no_anim = true })
|
||||||
|
|
||||||
|
require("dms.colors")
|
||||||
|
require("dms.outputs")
|
||||||
|
require("dms.layout")
|
||||||
|
require("dms.cursor")
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
require("dms.windowrules")
|
||||||
@@ -9,6 +9,9 @@ binds {
|
|||||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||||
}
|
}
|
||||||
|
Alt+Space hotkey-overlay-title="Spotlight Bar" {
|
||||||
|
spawn "dms" "ipc" "call" "spotlight-bar" "toggle";
|
||||||
|
}
|
||||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,26 @@ package config
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
//go:embed embedded/hyprland.conf
|
//go:embed embedded/hyprland.lua
|
||||||
var HyprlandConfig string
|
var HyprlandLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-colors.conf
|
//go:embed embedded/hypr-colors.lua
|
||||||
var HyprColorsConfig string
|
var DMSColorsLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-layout.conf
|
//go:embed embedded/hypr-layout.lua
|
||||||
var HyprLayoutConfig string
|
var DMSLayoutLuaConfig string
|
||||||
|
|
||||||
//go:embed embedded/hypr-binds.conf
|
//go:embed embedded/hypr-binds.lua
|
||||||
var HyprBindsConfig string
|
var DMSBindsLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-outputs.lua
|
||||||
|
var DMSOutputsLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-cursor.lua
|
||||||
|
var DMSCursorLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-windowrules.lua
|
||||||
|
var DMSWindowRulesLuaConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-binds-user.lua
|
||||||
|
var DMSBindsUserLuaConfig string
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hyprlandStartupBegin = "-- DMS_STARTUP_BEGIN"
|
||||||
|
hyprlandStartupEnd = "-- DMS_STARTUP_END"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractHyprlangMonitorLines(hyprlang string) []string {
|
||||||
|
re := regexp.MustCompile(`(?m)^\s*#?\s*monitor\s*=.*$`)
|
||||||
|
return re.FindAllString(hyprlang, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlangMonitorLineToLua(line string) (string, error) {
|
||||||
|
re := regexp.MustCompile(`(?i)^\s*#?\s*monitor\s*=\s*(.*)\s*$`)
|
||||||
|
m := re.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
return "", fmt.Errorf("not a monitor line")
|
||||||
|
}
|
||||||
|
rest := strings.TrimSpace(m[1])
|
||||||
|
parts := strings.Split(rest, ",")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
if len(parts) < 4 {
|
||||||
|
if len(parts) == 2 && strings.EqualFold(parts[1], "disable") {
|
||||||
|
return fmt.Sprintf(`hl.monitor({ output = %s, disabled = true })`, strconv.Quote(parts[0])), nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("expected at least 4 comma-separated fields")
|
||||||
|
}
|
||||||
|
out := parts[0]
|
||||||
|
mode := parts[1]
|
||||||
|
pos := parts[2]
|
||||||
|
scaleStr := parts[3]
|
||||||
|
|
||||||
|
scaleField := formatMonitorScaleLua(scaleStr)
|
||||||
|
fields := []string{
|
||||||
|
fmt.Sprintf("output = %s", strconv.Quote(out)),
|
||||||
|
fmt.Sprintf("mode = %s", strconv.Quote(mode)),
|
||||||
|
fmt.Sprintf("position = %s", strconv.Quote(pos)),
|
||||||
|
scaleField,
|
||||||
|
}
|
||||||
|
for i := 4; i < len(parts); i += 2 {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(parts[i]))
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i+1 >= len(parts) {
|
||||||
|
fields = append(fields, fmt.Sprintf("%s = true", hyprlangMonitorOptionToLuaKey(key)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(parts[i+1])
|
||||||
|
if converted, ok := formatMonitorOptionLua(key, val); ok {
|
||||||
|
fields = append(fields, converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`hl.monitor({ %s })`, strings.Join(fields, ", ")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMonitorScaleLua(scaleStr string) string {
|
||||||
|
if scaleStr == "auto" {
|
||||||
|
return `scale = "auto"`
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(scaleStr, 64); err == nil {
|
||||||
|
return fmt.Sprintf(`scale = %g`, f)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`scale = %s`, strconv.Quote(scaleStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlangMonitorOptionToLuaKey(key string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||||
|
case "10bit":
|
||||||
|
return "bitdepth"
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(key)), "-", "_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMonitorOptionLua(key, val string) (string, bool) {
|
||||||
|
luaKey := hyprlangMonitorOptionToLuaKey(key)
|
||||||
|
switch luaKey {
|
||||||
|
case "transform", "vrr", "bitdepth", "supports_wide_color", "supports_hdr", "sdr_max_luminance", "max_luminance", "max_avg_luminance":
|
||||||
|
if _, err := strconv.Atoi(val); err == nil {
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, val), true
|
||||||
|
}
|
||||||
|
case "sdrbrightness", "sdrsaturation", "sdr_min_luminance", "min_luminance":
|
||||||
|
if _, err := strconv.ParseFloat(val, 64); err == nil {
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, val), true
|
||||||
|
}
|
||||||
|
case "cm", "sdr_eotf", "icc", "mirror":
|
||||||
|
return fmt.Sprintf("%s = %s", luaKey, strconv.Quote(val)), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformHyprlandLuaForNonSystemd(config, terminalCommand string) string {
|
||||||
|
start := strings.Index(config, hyprlandStartupBegin)
|
||||||
|
end := strings.Index(config, hyprlandStartupEnd)
|
||||||
|
if start == -1 || end == -1 || end <= start {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
endClose := end + len(hyprlandStartupEnd)
|
||||||
|
replacement := hyprlandStartupBegin + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORM", "wayland;xcb")` + "\n" +
|
||||||
|
`hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")` + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORMTHEME", "gtk3")` + "\n" +
|
||||||
|
`hl.env("QT_QPA_PLATFORMTHEME_QT6", "gtk3")` + "\n" +
|
||||||
|
fmt.Sprintf(`hl.env("TERMINAL", %s)`, strconv.Quote(terminalCommand)) + "\n\n" +
|
||||||
|
`hl.on("hyprland.start", function()` + "\n" +
|
||||||
|
` hl.exec_cmd("dms run")` + "\n" +
|
||||||
|
`end)` + "\n" +
|
||||||
|
hyprlandStartupEnd
|
||||||
|
return config[:start] + replacement + config[endClose:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func readExistingHyprlandConfig(configDir string) (data string, sourcePath string, err error) {
|
||||||
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
if b, e := os.ReadFile(luaPath); e == nil {
|
||||||
|
return string(b), luaPath, nil
|
||||||
|
} else if !os.IsNotExist(e) {
|
||||||
|
return "", "", e
|
||||||
|
}
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if b, e := os.ReadFile(confPath); e == nil {
|
||||||
|
return string(b), confPath, nil
|
||||||
|
} else if !os.IsNotExist(e) {
|
||||||
|
return "", "", e
|
||||||
|
}
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configDir := filepath.Join(home, ".config", "hypr")
|
||||||
|
luaPath := filepath.Join(configDir, "hyprland.lua")
|
||||||
|
if _, err := os.Stat(luaPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(confPath); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts := time.Now().Format("2006-01-02_15-04-05")
|
||||||
|
dst := filepath.Join(configDir, hyprlandBackupDirName, ts, "hyprland.conf")
|
||||||
|
if err := moveHyprlandConfigFile(confPath, dst); err != nil {
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Could not move stray hyprland.conf: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Moved stray hyprland.conf to %s", dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
@@ -48,7 +49,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
|||||||
h.parsed = true
|
h.parsed = true
|
||||||
|
|
||||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
|
h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs, result.DefaultDMSKeys)
|
||||||
|
|
||||||
sheet := &keybinds.CheatSheet{
|
sheet := &keybinds.CheatSheet{
|
||||||
Title: "Hyprland Keybinds",
|
Title: "Hyprland Keybinds",
|
||||||
@@ -88,7 +89,7 @@ func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
|
|||||||
return h.dmsBindsIncluded
|
return h.dmsBindsIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
|
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) {
|
||||||
currentSubcat := subcategory
|
currentSubcat := subcategory
|
||||||
if section.Name != "" {
|
if section.Name != "" {
|
||||||
currentSubcat = section.Name
|
currentSubcat = section.Name
|
||||||
@@ -96,12 +97,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
|
|||||||
|
|
||||||
for _, kb := range section.Keybinds {
|
for _, kb := range section.Keybinds {
|
||||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||||
bind := h.convertKeybind(&kb, currentSubcat, conflicts)
|
bind := h.convertKeybind(&kb, currentSubcat, conflicts, defaultKeys)
|
||||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, child := range section.Children {
|
for _, child := range section.Children {
|
||||||
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
|
h.convertSection(&child, currentSubcat, categorizedBinds, conflicts, defaultKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
|
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding, defaultKeys map[string]bool) keybinds.Keybind {
|
||||||
keyStr := h.formatKey(kb)
|
keyStr := h.formatKey(kb)
|
||||||
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
|
||||||
desc := kb.Comment
|
desc := kb.Comment
|
||||||
@@ -143,8 +144,15 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
}
|
}
|
||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") {
|
if isDMSBindsUserOverridePath(kb.Source) {
|
||||||
source = "dms"
|
source = "dms"
|
||||||
|
} else if isDMSBindsPrimarySourcePath(kb.Source) {
|
||||||
|
source = "dms-default"
|
||||||
|
}
|
||||||
|
|
||||||
|
hasDefault := false
|
||||||
|
if source == "dms" && defaultKeys != nil {
|
||||||
|
hasDefault = defaultKeys[strings.ToLower(keyStr)]
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -154,9 +162,10 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
|
|||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
Source: source,
|
Source: source,
|
||||||
Flags: kb.Flags,
|
Flags: kb.Flags,
|
||||||
|
HasDefault: hasDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if (source == "dms" || source == "dms-default") && conflicts != nil {
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
@@ -188,9 +197,9 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
|||||||
func (h *HyprlandProvider) GetOverridePath() string {
|
func (h *HyprlandProvider) GetOverridePath() string {
|
||||||
expanded, err := utils.ExpandPath(h.configPath)
|
expanded, err := utils.ExpandPath(h.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filepath.Join(h.configPath, "dms", "binds.conf")
|
return filepath.Join(h.configPath, "dms", "binds-user.lua")
|
||||||
}
|
}
|
||||||
return filepath.Join(expanded, "dms", "binds.conf")
|
return filepath.Join(expanded, "dms", "binds-user.lua")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) validateAction(action string) error {
|
func (h *HyprlandProvider) validateAction(action string) error {
|
||||||
@@ -250,7 +259,16 @@ func (h *HyprlandProvider) RemoveBind(key string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
normalizedKey := strings.ToLower(key)
|
||||||
|
existingBinds[normalizedKey] = &hyprlandOverrideBind{Key: key, Unbind: true}
|
||||||
|
return h.writeOverrideBinds(existingBinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HyprlandProvider) ResetBind(key string) error {
|
||||||
|
existingBinds, err := h.loadOverrideBinds()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
normalizedKey := strings.ToLower(key)
|
normalizedKey := strings.ToLower(key)
|
||||||
delete(existingBinds, normalizedKey)
|
delete(existingBinds, normalizedKey)
|
||||||
return h.writeOverrideBinds(existingBinds)
|
return h.writeOverrideBinds(existingBinds)
|
||||||
@@ -262,116 +280,12 @@ type hyprlandOverrideBind struct {
|
|||||||
Description string
|
Description string
|
||||||
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press
|
||||||
Options map[string]any
|
Options map[string]any
|
||||||
|
// Unbind: negative override (hl.unbind only, no rebind).
|
||||||
|
Unbind bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
|
||||||
overridePath := h.GetOverridePath()
|
return readLuaOrHyprlangOverride(h.GetOverridePath())
|
||||||
binds := make(map[string]*hyprlandOverrideBind)
|
|
||||||
|
|
||||||
data, err := os.ReadFile(overridePath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(line, "bind") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract flags from bind type
|
|
||||||
bindType := strings.TrimSpace(parts[0])
|
|
||||||
flags := extractBindFlags(bindType)
|
|
||||||
hasDescFlag := strings.Contains(flags, "d")
|
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
|
||||||
|
|
||||||
var comment string
|
|
||||||
if len(commentParts) > 1 {
|
|
||||||
comment = strings.TrimSpace(commentParts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// For bindd, format is: mods, key, description, dispatcher, params
|
|
||||||
var minFields, descIndex, dispatcherIndex int
|
|
||||||
if hasDescFlag {
|
|
||||||
minFields = 4
|
|
||||||
descIndex = 2
|
|
||||||
dispatcherIndex = 3
|
|
||||||
} else {
|
|
||||||
minFields = 3
|
|
||||||
dispatcherIndex = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", minFields+2)
|
|
||||||
if len(fields) < minFields {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
|
||||||
keyName := strings.TrimSpace(fields[1])
|
|
||||||
|
|
||||||
var dispatcher, params string
|
|
||||||
if hasDescFlag {
|
|
||||||
if comment == "" {
|
|
||||||
comment = strings.TrimSpace(fields[descIndex])
|
|
||||||
}
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dispatcher = strings.TrimSpace(fields[dispatcherIndex])
|
|
||||||
if len(fields) > dispatcherIndex+1 {
|
|
||||||
paramParts := fields[dispatcherIndex+1:]
|
|
||||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStr := h.buildKeyString(mods, keyName)
|
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
|
||||||
action := dispatcher
|
|
||||||
if params != "" {
|
|
||||||
action = dispatcher + " " + params
|
|
||||||
}
|
|
||||||
|
|
||||||
binds[normalizedKey] = &hyprlandOverrideBind{
|
|
||||||
Key: keyStr,
|
|
||||||
Action: action,
|
|
||||||
Description: comment,
|
|
||||||
Flags: flags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
|
|
||||||
if mods == "" {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
modList := strings.FieldsFunc(mods, func(r rune) bool {
|
|
||||||
return r == '+' || r == ' '
|
|
||||||
})
|
|
||||||
|
|
||||||
parts := append(modList, key)
|
|
||||||
return strings.Join(parts, "+")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||||
@@ -420,78 +334,203 @@ func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverri
|
|||||||
})
|
})
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
sb.WriteString("-- DMS user keybind overrides (edit via Control Center or dms; do not remove this header)\n\n")
|
||||||
for _, bind := range bindList {
|
for _, bind := range bindList {
|
||||||
h.writeBindLine(&sb, bind)
|
writeLuaBindLine(&sb, bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
func formatLuaBindKey(internalKey string) string {
|
||||||
mods, key := h.parseKeyString(bind.Key)
|
internalKey = strings.TrimSpace(internalKey)
|
||||||
dispatcher, params := h.parseAction(bind.Action)
|
parts := strings.Split(internalKey, "+")
|
||||||
|
for i := range parts {
|
||||||
// Write bind type with flags (e.g., "bind", "binde", "bindel")
|
parts[i] = normalizeLuaBindKeyPart(strings.TrimSpace(parts[i]))
|
||||||
sb.WriteString("bind")
|
|
||||||
if bind.Flags != "" {
|
|
||||||
sb.WriteString(bind.Flags)
|
|
||||||
}
|
}
|
||||||
sb.WriteString(" = ")
|
return strings.Join(parts, " + ")
|
||||||
sb.WriteString(mods)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(key)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
|
|
||||||
// For bindd (description flag), include description before dispatcher
|
|
||||||
if strings.Contains(bind.Flags, "d") && bind.Description != "" {
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
sb.WriteString(", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString(dispatcher)
|
|
||||||
|
|
||||||
if params != "" {
|
|
||||||
sb.WriteString(", ")
|
|
||||||
sb.WriteString(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add comment if not using bindd (which has inline description)
|
|
||||||
if bind.Description != "" && !strings.Contains(bind.Flags, "d") {
|
|
||||||
sb.WriteString(" # ")
|
|
||||||
sb.WriteString(bind.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
|
func normalizeLuaBindKeyPart(part string) string {
|
||||||
parts := strings.Split(keyStr, "+")
|
switch strings.ToLower(part) {
|
||||||
switch len(parts) {
|
case "super", "mod4", "mainmod":
|
||||||
case 0:
|
return "SUPER"
|
||||||
return "", keyStr
|
case "ctrl", "control":
|
||||||
case 1:
|
return "CTRL"
|
||||||
return "", parts[0]
|
case "shift":
|
||||||
|
return "SHIFT"
|
||||||
|
case "alt", "mod1":
|
||||||
|
return "ALT"
|
||||||
|
}
|
||||||
|
if len(part) == 1 {
|
||||||
|
return strings.ToUpper(part)
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaActionStringFromHyprlangAction(action string) string {
|
||||||
|
action = strings.TrimSpace(action)
|
||||||
|
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:
|
default:
|
||||||
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
|
return fmt.Sprintf(`hl.dsp.exec_cmd(%s)`, strconv.Quote("hyprctl dispatch "+action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
|
func luaExprToInternalAction(expr string) string {
|
||||||
parts := strings.SplitN(action, " ", 2)
|
d, p := luaExprToDispatcherParams(expr)
|
||||||
switch len(parts) {
|
if d == "exec" && p != "" && !strings.HasPrefix(p, "hyprctl dispatch lua:") {
|
||||||
case 0:
|
return "exec " + p
|
||||||
return action, ""
|
|
||||||
case 1:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
default:
|
|
||||||
dispatcher = parts[0]
|
|
||||||
params = parts[1]
|
|
||||||
}
|
}
|
||||||
|
if p != "" {
|
||||||
// Convert internal spawn format to Hyprland's exec
|
return d + " " + p
|
||||||
if dispatcher == "spawn" {
|
|
||||||
dispatcher = "exec"
|
|
||||||
}
|
}
|
||||||
|
return d
|
||||||
return dispatcher, params
|
}
|
||||||
|
|
||||||
|
func luaBindOptions(bind *hyprlandOverrideBind) []string {
|
||||||
|
var opts []string
|
||||||
|
if strings.Contains(bind.Flags, "l") {
|
||||||
|
opts = append(opts, "locked = true")
|
||||||
|
}
|
||||||
|
if strings.Contains(bind.Flags, "e") {
|
||||||
|
opts = append(opts, "repeating = true")
|
||||||
|
}
|
||||||
|
if bind.Description != "" && strings.Contains(bind.Flags, "d") {
|
||||||
|
opts = append(opts, fmt.Sprintf("description = %s", strconv.Quote(bind.Description)))
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLuaBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
|
||||||
|
key := formatLuaBindKey(bind.Key)
|
||||||
|
if bind.Unbind {
|
||||||
|
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expr := luaActionStringFromHyprlangAction(bind.Action)
|
||||||
|
opts := luaBindOptions(bind)
|
||||||
|
fmt.Fprintf(sb, `hl.unbind("%s")`, key)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
if len(opts) > 0 {
|
||||||
|
fmt.Fprintf(sb, `hl.bind("%s", %s, { %s })`, key, expr, strings.Join(opts, ", "))
|
||||||
|
} else {
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLuaBindOverrideLine(line string) (*hyprlandOverrideBind, bool) {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "--") {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
kbc, actionExpr, optSuffix, ok := parseLuaBindInvocation(line)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
internalKey := luaKeyComboToInternalKey(kbc)
|
||||||
|
|
||||||
|
action := luaExprToInternalAction(actionExpr)
|
||||||
|
flags := luaBindOptFlags(optSuffix)
|
||||||
|
description := luaBindOptDescription(optSuffix)
|
||||||
|
return &hyprlandOverrideBind{
|
||||||
|
Key: internalKey,
|
||||||
|
Action: action,
|
||||||
|
Description: description,
|
||||||
|
Flags: flags,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLuaUnbindLine(line string) (string, bool) {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "hl.unbind") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest := strings.TrimSpace(line[len("hl.unbind"):])
|
||||||
|
if !strings.HasPrefix(rest, "(") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
rest = rest[1:]
|
||||||
|
combo, _, ok := parseLuaStringLiteral(rest, 0)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return luaKeyComboToInternalKey(combo), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToInternalKey(combo string) string {
|
||||||
|
parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(combo, "+", " "), " ", " "))
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLuaOrHyprlangOverride(path string) (map[string]*hyprlandOverrideBind, error) {
|
||||||
|
binds := make(map[string]*hyprlandOverrideBind)
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
parser := NewHyprlandParser("")
|
||||||
|
pendingUnbinds := make(map[string]string)
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "--") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key, ok := parseLuaUnbindLine(line); ok {
|
||||||
|
pendingUnbinds[strings.ToLower(key)] = key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kb, ok := parseLuaBindOverrideLine(line); ok {
|
||||||
|
normalizedKey := strings.ToLower(kb.Key)
|
||||||
|
binds[normalizedKey] = kb
|
||||||
|
delete(pendingUnbinds, normalizedKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(line, "bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kb := parser.parseBindLine(line)
|
||||||
|
if kb == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyStr := parser.formatBindKey(kb)
|
||||||
|
action := kb.Dispatcher
|
||||||
|
if kb.Params != "" {
|
||||||
|
action = kb.Dispatcher + " " + kb.Params
|
||||||
|
}
|
||||||
|
flags := kb.Flags
|
||||||
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
|
binds[normalizedKey] = &hyprlandOverrideBind{
|
||||||
|
Key: keyStr,
|
||||||
|
Action: action,
|
||||||
|
Description: kb.Comment,
|
||||||
|
Flags: flags,
|
||||||
|
}
|
||||||
|
delete(pendingUnbinds, normalizedKey)
|
||||||
|
}
|
||||||
|
for normKey, origKey := range pendingUnbinds {
|
||||||
|
binds[normKey] = &hyprlandOverrideBind{Key: origKey, Unbind: true}
|
||||||
|
}
|
||||||
|
return binds, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/luaconfig"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,6 +52,8 @@ type HyprlandParser struct {
|
|||||||
bindOrder []string
|
bindOrder []string
|
||||||
processedFiles map[string]bool
|
processedFiles map[string]bool
|
||||||
dmsProcessed bool
|
dmsProcessed bool
|
||||||
|
removedKeys map[string]bool // bare hl.unbind targets (negative overrides)
|
||||||
|
defaultDMSKeys map[string]bool // keys present in dms/binds.{lua,conf}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHyprlandParser(configDir string) *HyprlandParser {
|
func NewHyprlandParser(configDir string) *HyprlandParser {
|
||||||
@@ -64,6 +68,8 @@ func NewHyprlandParser(configDir string) *HyprlandParser {
|
|||||||
bindMap: make(map[string]*HyprlandKeyBinding),
|
bindMap: make(map[string]*HyprlandKeyBinding),
|
||||||
bindOrder: []string{},
|
bindOrder: []string{},
|
||||||
processedFiles: make(map[string]bool),
|
processedFiles: make(map[string]bool),
|
||||||
|
removedKeys: make(map[string]bool),
|
||||||
|
defaultDMSKeys: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +298,7 @@ type HyprlandParseResult struct {
|
|||||||
DMSBindsIncluded bool
|
DMSBindsIncluded bool
|
||||||
DMSStatus *HyprlandDMSStatus
|
DMSStatus *HyprlandDMSStatus
|
||||||
ConflictingConfigs map[string]*HyprlandKeyBinding
|
ConflictingConfigs map[string]*HyprlandKeyBinding
|
||||||
|
DefaultDMSKeys map[string]bool // keys with a DMS default in binds.{lua,conf}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HyprlandDMSStatus struct {
|
type HyprlandDMSStatus struct {
|
||||||
@@ -317,10 +324,10 @@ func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
|
|||||||
switch {
|
switch {
|
||||||
case !p.dmsBindsExists:
|
case !p.dmsBindsExists:
|
||||||
status.Effective = false
|
status.Effective = false
|
||||||
status.StatusMessage = "dms/binds.conf does not exist"
|
status.StatusMessage = "dms/binds.lua (or legacy binds.conf) does not exist"
|
||||||
case !p.dmsBindsIncluded:
|
case !p.dmsBindsIncluded:
|
||||||
status.Effective = false
|
status.Effective = false
|
||||||
status.StatusMessage = "dms/binds.conf is not sourced in config"
|
status.StatusMessage = "dms binds are not loaded from Hyprland config (require / source)"
|
||||||
case p.bindsAfterDMS > 0:
|
case p.bindsAfterDMS > 0:
|
||||||
status.Effective = true
|
status.Effective = true
|
||||||
status.OverriddenBy = p.bindsAfterDMS
|
status.OverriddenBy = p.bindsAfterDMS
|
||||||
@@ -347,8 +354,11 @@ func (p *HyprlandParser) normalizeKey(key string) string {
|
|||||||
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
|
||||||
key := p.formatBindKey(kb)
|
key := p.formatBindKey(kb)
|
||||||
normalizedKey := p.normalizeKey(key)
|
normalizedKey := p.normalizeKey(key)
|
||||||
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf")
|
isDMSBind := isDMSBindsSourcePath(kb.Source)
|
||||||
|
|
||||||
|
if isDMSBindsPrimarySourcePath(kb.Source) {
|
||||||
|
p.defaultDMSKeys[normalizedKey] = true
|
||||||
|
}
|
||||||
if isDMSBind {
|
if isDMSBind {
|
||||||
p.dmsBindKeys[normalizedKey] = true
|
p.dmsBindKeys[normalizedKey] = true
|
||||||
} else if p.dmsBindKeys[normalizedKey] {
|
} else if p.dmsBindKeys[normalizedKey] {
|
||||||
@@ -373,12 +383,21 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
|
dmsBindsLua := filepath.Join(expandedDir, "dms", "binds.lua")
|
||||||
if _, err := os.Stat(dmsBindsPath); err == nil {
|
dmsBindsConf := filepath.Join(expandedDir, "dms", "binds.conf")
|
||||||
|
dmsBindsPath := ""
|
||||||
|
if _, err := os.Stat(dmsBindsLua); err == nil {
|
||||||
p.dmsBindsExists = true
|
p.dmsBindsExists = true
|
||||||
|
dmsBindsPath = dmsBindsLua
|
||||||
|
} else if _, err := os.Stat(dmsBindsConf); err == nil {
|
||||||
|
p.dmsBindsExists = true
|
||||||
|
dmsBindsPath = dmsBindsConf
|
||||||
}
|
}
|
||||||
|
|
||||||
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
|
mainConfig, err := hyprlandMainConfigPath(p.configDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
section, err := p.parseFileWithSource(mainConfig, "")
|
section, err := p.parseFileWithSource(mainConfig, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -387,10 +406,65 @@ func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
|
|||||||
if p.dmsBindsExists && !p.dmsProcessed {
|
if p.dmsBindsExists && !p.dmsProcessed {
|
||||||
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
p.parseDMSBindsDirectly(dmsBindsPath, section)
|
||||||
}
|
}
|
||||||
|
p.removeShadowedDMSBinds(section)
|
||||||
|
p.removeUnboundDMSBinds(section)
|
||||||
|
|
||||||
return section, nil
|
return section, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) removeUnboundDMSBinds(section *HyprlandSection) {
|
||||||
|
if len(p.removedKeys) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filtered := section.Keybinds[:0]
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := section.Keybinds[i]
|
||||||
|
if isDMSBindsSourcePath(kb.Source) && p.removedKeys[p.normalizeKey(p.formatBindKey(&kb))] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, kb)
|
||||||
|
}
|
||||||
|
section.Keybinds = filtered
|
||||||
|
for i := range section.Children {
|
||||||
|
p.removeUnboundDMSBinds(§ion.Children[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) removeShadowedDMSBinds(section *HyprlandSection) {
|
||||||
|
counts := make(map[string]int)
|
||||||
|
p.countDMSBinds(section, counts)
|
||||||
|
p.filterShadowedDMSBinds(section, counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) countDMSBinds(section *HyprlandSection, counts map[string]int) {
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := §ion.Keybinds[i]
|
||||||
|
if isDMSBindsSourcePath(kb.Source) {
|
||||||
|
counts[p.normalizeKey(p.formatBindKey(kb))]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range section.Children {
|
||||||
|
p.countDMSBinds(§ion.Children[i], counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) filterShadowedDMSBinds(section *HyprlandSection, counts map[string]int) {
|
||||||
|
filtered := section.Keybinds[:0]
|
||||||
|
for i := range section.Keybinds {
|
||||||
|
kb := section.Keybinds[i]
|
||||||
|
key := p.normalizeKey(p.formatBindKey(&kb))
|
||||||
|
if isDMSBindsSourcePath(kb.Source) && counts[key] > 1 {
|
||||||
|
counts[key]--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, kb)
|
||||||
|
}
|
||||||
|
section.Keybinds = filtered
|
||||||
|
for i := range section.Children {
|
||||||
|
p.filterShadowedDMSBinds(§ion.Children[i], counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
|
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
|
||||||
absPath, err := filepath.Abs(filePath)
|
absPath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -407,6 +481,10 @@ func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*Hyp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(filepath.Ext(absPath), ".lua") {
|
||||||
|
return p.parseLuaLines(string(data), filepath.Dir(absPath), absPath, sectionName)
|
||||||
|
}
|
||||||
|
|
||||||
prevSource := p.currentSource
|
prevSource := p.currentSource
|
||||||
p.currentSource = absPath
|
p.currentSource = absPath
|
||||||
|
|
||||||
@@ -446,7 +524,7 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourcePath := strings.TrimSpace(parts[1])
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
|
isDMSSource := isDMSBindsPrimarySourcePath(sourcePath)
|
||||||
|
|
||||||
p.includeCount++
|
p.includeCount++
|
||||||
if isDMSSource {
|
if isDMSSource {
|
||||||
@@ -474,6 +552,17 @@ func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, bas
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
|
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
|
||||||
|
if strings.EqualFold(filepath.Ext(dmsBindsPath), ".lua") {
|
||||||
|
sub, err := p.parseLuaLinesFromPath(dmsBindsPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
section.Keybinds = append(section.Keybinds, sub.Keybinds...)
|
||||||
|
section.Children = append(section.Children, sub.Children...)
|
||||||
|
p.dmsProcessed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(dmsBindsPath)
|
data, err := os.ReadFile(dmsBindsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -503,6 +592,124 @@ func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *Hyp
|
|||||||
p.dmsProcessed = true
|
p.dmsProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HyprlandParser) parseLuaLinesFromPath(absPath string) (*HyprlandSection, error) {
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.parseLuaLines(string(data), filepath.Dir(absPath), absPath, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaLines reads a Hyprland Lua config fragment: require() includes and hl.bind keybinds.
|
||||||
|
func (p *HyprlandParser) parseLuaLines(content string, baseDir, absPath, sectionName string) (*HyprlandSection, error) {
|
||||||
|
section := &HyprlandSection{Name: sectionName}
|
||||||
|
prevSource := p.currentSource
|
||||||
|
p.currentSource = absPath
|
||||||
|
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
boundInFile := make(map[string]bool)
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "--") || !strings.Contains(trimmed, "hl.bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if kbc, _, _, ok := parseLuaBindInvocation(trimmed); ok {
|
||||||
|
boundInFile[strings.ToLower(luaKeyComboToInternalKey(kbc))] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootDir := baseDir
|
||||||
|
if expanded, err := utils.ExpandPath(p.configDir); err == nil && expanded != "" {
|
||||||
|
rootDir = expanded
|
||||||
|
}
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "--") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if modules := luaconfig.Requires(trimmed); len(modules) > 0 {
|
||||||
|
for _, mod := range modules {
|
||||||
|
rel := luaconfig.ModuleToRelPath(mod)
|
||||||
|
if rel == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isDMS := isDMSBindsPrimarySourcePath(rel)
|
||||||
|
p.includeCount++
|
||||||
|
if isDMS {
|
||||||
|
p.dmsBindsIncluded = true
|
||||||
|
p.dmsIncludePos = p.includeCount
|
||||||
|
p.dmsProcessed = true
|
||||||
|
}
|
||||||
|
fullPath := luaconfig.ModuleToPath(rootDir, mod)
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
includedSection, err := p.parseFileWithSource(expanded, "")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
section.Children = append(section.Children, *includedSection)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "hl.unbind") {
|
||||||
|
if key, ok := parseLuaUnbindLine(trimmed); ok {
|
||||||
|
normalized := strings.ToLower(key)
|
||||||
|
if !boundInFile[normalized] {
|
||||||
|
p.removedKeys[normalized] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(trimmed, "hl.bind") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kbc, action, optSuffix, ok := parseLuaBindInvocation(trimmed)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flags := luaBindOptFlags(optSuffix)
|
||||||
|
desc := luaBindOptDescription(optSuffix)
|
||||||
|
if desc == "" {
|
||||||
|
desc = luaLineTrailingComment(line)
|
||||||
|
}
|
||||||
|
kb := luaKeyComboToBinding(kbc, action, p.currentSource, desc)
|
||||||
|
kb.Flags = flags
|
||||||
|
if p.addBind(kb) {
|
||||||
|
section.Keybinds = append(section.Keybinds, *kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentSource = prevSource
|
||||||
|
return section, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaBindOptFlags(optSuffix string) string {
|
||||||
|
optSuffix = strings.TrimSpace(optSuffix)
|
||||||
|
if optSuffix == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var flags string
|
||||||
|
if strings.Contains(optSuffix, "repeating") {
|
||||||
|
flags += "e"
|
||||||
|
}
|
||||||
|
if strings.Contains(optSuffix, "locked") {
|
||||||
|
flags += "l"
|
||||||
|
}
|
||||||
|
if strings.Contains(optSuffix, "description") {
|
||||||
|
flags += "d"
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaBindOptDescription(optSuffix string) string {
|
||||||
|
return luaTableStringField(optSuffix, "description")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
|
||||||
parts := strings.SplitN(line, "=", 2)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
@@ -623,5 +830,356 @@ func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
|
|||||||
DMSBindsIncluded: parser.dmsBindsIncluded,
|
DMSBindsIncluded: parser.dmsBindsIncluded,
|
||||||
DMSStatus: parser.buildDMSStatus(),
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
ConflictingConfigs: parser.conflictingConfigs,
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
|
DefaultDMSKeys: parser.defaultDMSKeys,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func skipLuaWS(s string, i int) int {
|
||||||
|
for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaStringLiteral reads a Lua "..." or '...' starting at i (first quote).
|
||||||
|
func parseLuaStringLiteral(line string, i int) (value string, next int, ok bool) {
|
||||||
|
if i >= len(line) {
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
q := line[i]
|
||||||
|
if q != '"' && q != '\'' {
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
var sb strings.Builder
|
||||||
|
for i < len(line) {
|
||||||
|
c := line[i]
|
||||||
|
if c == '\\' && i+1 < len(line) {
|
||||||
|
i++
|
||||||
|
sb.WriteByte(line[i])
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == q {
|
||||||
|
return sb.String(), i + 1, true
|
||||||
|
}
|
||||||
|
sb.WriteByte(c)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return "", i, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
if esc {
|
||||||
|
esc = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '\\' && inStr == '"' {
|
||||||
|
esc = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == inStr {
|
||||||
|
inStr = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '"', '\'':
|
||||||
|
inStr = c
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 {
|
||||||
|
return strings.TrimSpace(line[exprStart : i+1]), i + 1, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", start, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLuaBindInvocation parses one hl.bind("KEY", expr [, opts]) on a single line.
|
||||||
|
func parseLuaBindInvocation(line string) (keyCombo, actionExpr, optSuffix string, ok bool) {
|
||||||
|
idx := strings.Index(line, "hl.bind")
|
||||||
|
if idx < 0 {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i := idx + len("hl.bind")
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i >= len(line) || line[i] != '(' {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
keyCombo, i, ok = parseLuaStringLiteral(line, i)
|
||||||
|
if !ok {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i >= len(line) || line[i] != ',' {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
actionExpr, i, ok = parseLuaFirstArgExpr(line, i)
|
||||||
|
if !ok {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
i = skipLuaWS(line, i)
|
||||||
|
if i < len(line) && line[i] == ',' {
|
||||||
|
optSuffix = strings.TrimSpace(line[i:])
|
||||||
|
}
|
||||||
|
return keyCombo, strings.TrimSpace(actionExpr), optSuffix, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToBinding(keyCombo, actionExpr, source, lineComment string) *HyprlandKeyBinding {
|
||||||
|
keyCombo = strings.TrimSpace(keyCombo)
|
||||||
|
mods, leaf := luaKeyComboToModsKey(keyCombo)
|
||||||
|
dispatcher, params := luaExprToDispatcherParams(actionExpr)
|
||||||
|
comment := lineComment
|
||||||
|
if comment == "" {
|
||||||
|
comment = hyprlandAutogenerateComment(dispatcher, params)
|
||||||
|
}
|
||||||
|
return &HyprlandKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: leaf,
|
||||||
|
Dispatcher: dispatcher,
|
||||||
|
Params: params,
|
||||||
|
Comment: comment,
|
||||||
|
Source: source,
|
||||||
|
Flags: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaKeyComboToModsKey(combo string) (mods []string, leaf string) {
|
||||||
|
parts := strings.Split(combo, "+")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
switch len(parts) {
|
||||||
|
case 0:
|
||||||
|
return nil, ""
|
||||||
|
case 1:
|
||||||
|
return nil, parts[0]
|
||||||
|
default:
|
||||||
|
return parts[:len(parts)-1], parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaExprToDispatcherParams(expr string) (dispatcher, params string) {
|
||||||
|
expr = strings.TrimSpace(expr)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.exec_cmd("):
|
||||||
|
arg := extractLuaCallStringArg(expr, "hl.dsp.exec_cmd")
|
||||||
|
if arg != "" {
|
||||||
|
if u, err := strconv.Unquote(arg); err == nil {
|
||||||
|
if strings.HasPrefix(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.Contains(expr, "hl.dsp.window.kill()"):
|
||||||
|
return "killactive", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.fullscreen("):
|
||||||
|
switch luaTableStringField(expr, "mode") {
|
||||||
|
case "maximized", "maximize":
|
||||||
|
return "fullscreen", "1"
|
||||||
|
case "fullscreen":
|
||||||
|
return "fullscreen", "0"
|
||||||
|
}
|
||||||
|
return "fullscreen", luaTableStringField(expr, "mode")
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.float("):
|
||||||
|
return "togglefloating", ""
|
||||||
|
case strings.Contains(expr, "hl.dsp.group.toggle()"):
|
||||||
|
return "togglegroup", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.focus("):
|
||||||
|
switch {
|
||||||
|
case luaTableStringField(expr, "direction") != "":
|
||||||
|
return "movefocus", luaTableStringField(expr, "direction")
|
||||||
|
case luaTableStringField(expr, "monitor") != "":
|
||||||
|
return "focusmonitor", luaTableStringField(expr, "monitor")
|
||||||
|
case luaTableStringField(expr, "workspace") != "":
|
||||||
|
return "workspace", luaTableStringField(expr, "workspace")
|
||||||
|
case luaTableStringField(expr, "window") != "":
|
||||||
|
return "focuswindow", luaTableStringField(expr, "window")
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.move("):
|
||||||
|
switch {
|
||||||
|
case luaTableStringField(expr, "direction") != "":
|
||||||
|
return "movewindow", luaTableStringField(expr, "direction")
|
||||||
|
case luaTableStringField(expr, "monitor") != "":
|
||||||
|
return "movewindow", "mon:" + luaTableStringField(expr, "monitor")
|
||||||
|
case luaTableStringField(expr, "workspace") != "":
|
||||||
|
return "movetoworkspace", luaTableStringField(expr, "workspace")
|
||||||
|
}
|
||||||
|
case expr == "hl.dsp.window.drag()":
|
||||||
|
return "movewindow", ""
|
||||||
|
case expr == "hl.dsp.window.resize()":
|
||||||
|
return "resizewindow", ""
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.window.resize("):
|
||||||
|
x := luaStringValue(luaTableScalarField(expr, "x"))
|
||||||
|
y := luaStringValue(luaTableScalarField(expr, "y"))
|
||||||
|
if x != "" || y != "" {
|
||||||
|
if x == "" {
|
||||||
|
x = "0"
|
||||||
|
}
|
||||||
|
if y == "" {
|
||||||
|
y = "0"
|
||||||
|
}
|
||||||
|
return "resizeactive", x + " " + y
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(expr, "hl.dsp.layout("):
|
||||||
|
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 != "" {
|
||||||
|
return "dpms", action
|
||||||
|
}
|
||||||
|
case strings.Contains(expr, "hl.dsp.exit()"):
|
||||||
|
return "exit", ""
|
||||||
|
default:
|
||||||
|
return "exec", "hyprctl dispatch lua:" + expr
|
||||||
|
}
|
||||||
|
return "exec", "hyprctl dispatch lua:" + expr
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractLuaCallStringArg(callExpr, funcName string) string {
|
||||||
|
callExpr = strings.TrimSpace(callExpr)
|
||||||
|
prefix := funcName + "("
|
||||||
|
if !strings.HasPrefix(callExpr, prefix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
inner := callExpr[len(prefix):]
|
||||||
|
inner = strings.TrimSpace(inner)
|
||||||
|
if len(inner) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch inner[0] {
|
||||||
|
case '"', '\'':
|
||||||
|
s, _, ok := parseLuaStringLiteral(inner, 0)
|
||||||
|
if ok {
|
||||||
|
return strconv.Quote(s)
|
||||||
|
}
|
||||||
|
case '[':
|
||||||
|
if strings.HasPrefix(inner, "[[") {
|
||||||
|
if end := strings.Index(inner[2:], "]]"); end >= 0 {
|
||||||
|
return strconv.Quote(inner[2 : 2+end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaTableStringField(expr, field string) string {
|
||||||
|
return luaStringValue(luaTableScalarField(expr, field))
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaTableScalarField(expr, field string) string {
|
||||||
|
re := regexp.MustCompile(`(?s)\b` + regexp.QuoteMeta(field) + `\s*=\s*("(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\[\[.*?\]\]|-?\d+(?:\.\d+)?|true|false)`)
|
||||||
|
m := re.FindStringSubmatch(expr)
|
||||||
|
if len(m) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(m[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaStringValue(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(raw, "[[") && strings.HasSuffix(raw, "]]") {
|
||||||
|
return raw[2 : len(raw)-2]
|
||||||
|
}
|
||||||
|
if len(raw) >= 2 {
|
||||||
|
q := raw[0]
|
||||||
|
if (q == '"' || q == '\'') && raw[len(raw)-1] == q {
|
||||||
|
if q == '"' {
|
||||||
|
if u, err := strconv.Unquote(raw); err == nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(raw[1:len(raw)-1], `\'`, `'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func luaLineTrailingComment(line string) string {
|
||||||
|
if idx := strings.Index(line, "--"); idx >= 0 {
|
||||||
|
return strings.TrimSpace(line[idx+2:])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsSourcePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
if isDMSBindsPrimarySourcePath(p) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isDMSBindsUserOverridePath(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsUserOverridePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
return p == "dms/binds-user.lua" || p == "./dms/binds-user.lua" ||
|
||||||
|
strings.HasSuffix(p, "/dms/binds-user.lua")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDMSBindsPrimarySourcePath(p string) bool {
|
||||||
|
p = filepath.ToSlash(strings.TrimSpace(p))
|
||||||
|
if strings.Contains(p, "/dms/binds.lua") || strings.HasSuffix(p, "dms/binds.lua") || p == "dms/binds.lua" || p == "./dms/binds.lua" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(p, "/dms/binds.conf") || strings.HasSuffix(p, "dms/binds.conf") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p == "dms/binds.conf" || p == "./dms/binds.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
// hyprlandMainConfigPath returns hyprland.lua if present, else hyprland.conf if present.
|
||||||
|
func hyprlandMainConfigPath(dir string) (string, error) {
|
||||||
|
expandedDir, err := utils.ExpandPath(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
luaPath := filepath.Join(expandedDir, "hyprland.lua")
|
||||||
|
if st, err := os.Stat(luaPath); err == nil && st.Mode().IsRegular() {
|
||||||
|
return luaPath, nil
|
||||||
|
}
|
||||||
|
confPath := filepath.Join(expandedDir, "hyprland.conf")
|
||||||
|
if st, err := os.Stat(confPath); err == nil && st.Mode().IsRegular() {
|
||||||
|
return confPath, nil
|
||||||
|
}
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHyprlandAutogenerateComment(t *testing.T) {
|
func TestHyprlandAutogenerateComment(t *testing.T) {
|
||||||
@@ -60,6 +63,341 @@ func TestHyprlandAutogenerateComment(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHyprlandLuaBindRoundTripHelpers(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
expr string
|
||||||
|
wantDispatcher string
|
||||||
|
wantParams string
|
||||||
|
}{
|
||||||
|
{`hl.dsp.exec_cmd([[dms ipc call brightness increment 5 ""]])`, "exec", `dms ipc call brightness increment 5 ""`},
|
||||||
|
{`hl.dsp.window.fullscreen({ mode = "maximized", action = "toggle" })`, "fullscreen", "1"},
|
||||||
|
{`hl.dsp.focus({ workspace = "e+1" })`, "workspace", "e+1"},
|
||||||
|
{`hl.dsp.window.move({ monitor = "l" })`, "movewindow", "mon:l"},
|
||||||
|
{`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"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.expr, func(t *testing.T) {
|
||||||
|
gotDispatcher, gotParams := luaExprToDispatcherParams(tt.expr)
|
||||||
|
if gotDispatcher != tt.wantDispatcher || gotParams != tt.wantParams {
|
||||||
|
t.Fatalf("luaExprToDispatcherParams() = %q, %q; want %q, %q", gotDispatcher, gotParams, tt.wantDispatcher, tt.wantParams)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineOptionsInsideCall(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{
|
||||||
|
Key: "Super+k",
|
||||||
|
Action: "exec kitty",
|
||||||
|
Description: "Open terminal",
|
||||||
|
Flags: "led",
|
||||||
|
})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + K")
|
||||||
|
hl.bind("SUPER + K", hl.dsp.exec_cmd("kitty"), { locked = true, repeating = true, description = "Open terminal" })`
|
||||||
|
if got := strings.TrimSpace(sb.String()); got != want {
|
||||||
|
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineMapsSpawnActionForHyprland(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{
|
||||||
|
Key: "Super+n",
|
||||||
|
Action: "spawn dms ipc call notepad toggle",
|
||||||
|
Description: "Notepad: Toggle",
|
||||||
|
})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + N")
|
||||||
|
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 TestHyprlandLuaBindsUserOverridesDefaults(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(`hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(`hl.bind("SUPER + T", hl.dsp.exec_cmd("foot"), { description = "User terminal" })`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseHyprlandKeysWithDMS(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var found []HyprlandKeyBinding
|
||||||
|
var walk func(HyprlandSection)
|
||||||
|
walk = func(section HyprlandSection) {
|
||||||
|
for _, kb := range section.Keybinds {
|
||||||
|
if strings.EqualFold(strings.Join(append(kb.Mods, kb.Key), "+"), "SUPER+T") {
|
||||||
|
found = append(found, kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, child := range section.Children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(*result.Section)
|
||||||
|
|
||||||
|
if len(found) != 1 {
|
||||||
|
t.Fatalf("expected one effective SUPER+T bind, got %d: %#v", len(found), found)
|
||||||
|
}
|
||||||
|
if found[0].Params != "foot" || found[0].Comment != "User terminal" {
|
||||||
|
t.Fatalf("expected user override bind, got %#v", found[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteLuaBindLineEmitsUnbindOnlyForNegativeOverride(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
writeLuaBindLine(&sb, &hyprlandOverrideBind{Key: "Super+i", Unbind: true})
|
||||||
|
|
||||||
|
want := `hl.unbind("SUPER + I")`
|
||||||
|
if got := strings.TrimSpace(sb.String()); got != want {
|
||||||
|
t.Fatalf("writeLuaBindLine() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadLuaOverrideRecognizesLoneUnbindAsNegativeOverride(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
overridePath := filepath.Join(tmpDir, "binds-user.lua")
|
||||||
|
contents := `-- DMS user keybind overrides
|
||||||
|
hl.unbind("SUPER + I")
|
||||||
|
hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(overridePath, []byte(contents), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binds, err := readLuaOrHyprlangOverride(overridePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, ok := binds["super+i"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected SUPER+I entry in override map, got: %#v", binds)
|
||||||
|
}
|
||||||
|
if !got.Unbind {
|
||||||
|
t.Fatalf("expected SUPER+I to be marked Unbind, got: %#v", got)
|
||||||
|
}
|
||||||
|
if rebind, ok := binds["super+n"]; !ok || rebind.Unbind {
|
||||||
|
t.Fatalf("expected SUPER+N to be a normal rebind override, got: %#v", rebind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParserDropsDMSDefaultsSuppressedByBindsUserUnbind(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(
|
||||||
|
`hl.bind("SUPER + I", hl.dsp.focus({ workspace = "e-1" }))
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(`hl.unbind("SUPER + I")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseHyprlandKeysWithDMS(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
var walk func(HyprlandSection)
|
||||||
|
walk = func(section HyprlandSection) {
|
||||||
|
for _, kb := range section.Keybinds {
|
||||||
|
keys = append(keys, strings.ToUpper(strings.Join(append(kb.Mods, kb.Key), "+")))
|
||||||
|
}
|
||||||
|
for _, child := range section.Children {
|
||||||
|
walk(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(*result.Section)
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
if k == "SUPER+I" {
|
||||||
|
t.Fatalf("expected SUPER+I to be suppressed by binds-user.lua unbind, got: %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundT := false
|
||||||
|
for _, k := range keys {
|
||||||
|
if k == "SUPER+T" {
|
||||||
|
foundT = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundT {
|
||||||
|
t.Fatalf("expected SUPER+T to remain (only SUPER+I was unbound), got: %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandRemoveBindWritesNegativeOverrideForDefault(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.RemoveBind("SUPER+I"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `hl.unbind("SUPER + I")`) {
|
||||||
|
t.Fatalf("expected negative override hl.unbind line, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `hl.bind("SUPER + I"`) {
|
||||||
|
t.Fatalf("expected NO hl.bind for SUPER+I, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandRemoveBindReplacesExistingOverrideWithNegativeOverride(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
override := `hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.RemoveBind("SUPER+N"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `hl.unbind("SUPER + N")`) {
|
||||||
|
t.Fatalf("expected negative override hl.unbind line, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `hl.bind("SUPER + N"`) {
|
||||||
|
t.Fatalf("expected NO hl.bind for SUPER+N after remove, got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandResetBindRevertsExistingOverrideToDefault(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
override := `hl.unbind("SUPER + N")
|
||||||
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(override), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
if err := provider.ResetBind("SUPER+N"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dmsDir, "binds-user.lua"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), `SUPER + N`) {
|
||||||
|
t.Fatalf("expected SUPER+N to be fully removed (revert to default), got:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyprlandHasDefaultSetForOverrideOfDefaultKey(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.lua"), []byte(
|
||||||
|
`hl.bind("SUPER + T", hl.dsp.exec_cmd("kitty"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds-user.lua"), []byte(
|
||||||
|
`hl.unbind("SUPER + T")
|
||||||
|
hl.bind("SUPER + T", hl.dsp.exec_cmd("foot"))
|
||||||
|
hl.bind("SUPER + Z", hl.dsp.exec_cmd("custom"))`,
|
||||||
|
), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewHyprlandProvider(tmpDir)
|
||||||
|
sheet, err := provider.GetCheatSheet()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundT, foundZ *keybinds.Keybind
|
||||||
|
for _, group := range sheet.Binds {
|
||||||
|
for i := range group {
|
||||||
|
kb := group[i]
|
||||||
|
keyUpper := strings.ToUpper(kb.Key)
|
||||||
|
if keyUpper == "SUPER+T" {
|
||||||
|
foundT = &group[i]
|
||||||
|
}
|
||||||
|
if keyUpper == "SUPER+Z" {
|
||||||
|
foundZ = &group[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundT == nil {
|
||||||
|
t.Fatalf("expected SUPER+T override in cheatsheet")
|
||||||
|
}
|
||||||
|
if !foundT.HasDefault {
|
||||||
|
t.Fatalf("expected SUPER+T HasDefault=true (default exists in binds.lua), got %+v", foundT)
|
||||||
|
}
|
||||||
|
if foundZ == nil {
|
||||||
|
t.Fatalf("expected SUPER+Z (user-only) in cheatsheet")
|
||||||
|
}
|
||||||
|
if foundZ.HasDefault {
|
||||||
|
t.Fatalf("expected SUPER+Z HasDefault=false (no default), got %+v", foundZ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
|||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
|
||||||
source = "dms"
|
source = "dms-default"
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -151,7 +151,7 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[st
|
|||||||
Source: source,
|
Source: source,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms-default" && conflicts != nil {
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
normalizedKey := strings.ToLower(keyStr)
|
||||||
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
if conflictKb, ok := conflicts[normalizedKey]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
@@ -249,6 +249,10 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
|
|||||||
return m.writeOverrideBinds(existingBinds)
|
return m.writeOverrideBinds(existingBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) ResetBind(key string) error {
|
||||||
|
return m.RemoveBind(key)
|
||||||
|
}
|
||||||
|
|
||||||
type mangowcOverrideBind struct {
|
type mangowcOverrideBind struct {
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
|||||||
|
|
||||||
source := "config"
|
source := "config"
|
||||||
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
if strings.Contains(kb.Source, "dms/binds.kdl") {
|
||||||
source = "dms"
|
source = "dms-default"
|
||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
@@ -165,8 +165,8 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
|||||||
Repeat: kb.Repeat,
|
Repeat: kb.Repeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms-default" && conflicts != nil {
|
||||||
if conflictKb, ok := conflicts[keyStr]; ok {
|
if conflictKb, ok := conflicts[normalizeNiriBindKey(keyStr)]; ok {
|
||||||
bind.Conflict = &keybinds.Keybind{
|
bind.Conflict = &keybinds.Keybind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Description: conflictKb.Description,
|
Description: conflictKb.Description,
|
||||||
@@ -249,7 +249,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
|
|||||||
existingBinds = make(map[string]*overrideBind)
|
existingBinds = make(map[string]*overrideBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
existingBinds[key] = &overrideBind{
|
existingBinds[normalizeNiriBindKey(key)] = &overrideBind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: description,
|
Description: description,
|
||||||
@@ -265,10 +265,14 @@ func (n *NiriProvider) RemoveBind(key string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(existingBinds, key)
|
delete(existingBinds, normalizeNiriBindKey(key))
|
||||||
return n.writeOverrideBinds(existingBinds)
|
return n.writeOverrideBinds(existingBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NiriProvider) ResetBind(key string) error {
|
||||||
|
return n.RemoveBind(key)
|
||||||
|
}
|
||||||
|
|
||||||
type overrideBind struct {
|
type overrideBind struct {
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
@@ -312,7 +316,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
action = n.formatRawAction(kb.Action, kb.Args)
|
action = n.formatRawAction(kb.Action, kb.Args)
|
||||||
}
|
}
|
||||||
|
|
||||||
binds[keyStr] = &overrideBind{
|
binds[normalizeNiriBindKey(keyStr)] = &overrideBind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: kb.Description,
|
Description: kb.Description,
|
||||||
|
|||||||
@@ -162,6 +162,14 @@ func NewNiriParser(configDir string) *NiriParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeNiriBindKey(key string) string {
|
||||||
|
parts := strings.Split(key, "+")
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.ToLower(strings.TrimSpace(parts[i]))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *NiriParser) Parse() (*NiriSection, error) {
|
func (p *NiriParser) Parse() (*NiriSection, error) {
|
||||||
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
|
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
|
||||||
if _, err := os.Stat(dmsBindsPath); err == nil {
|
if _, err := os.Stat(dmsBindsPath); err == nil {
|
||||||
@@ -213,24 +221,25 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
|
|||||||
|
|
||||||
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
|
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
|
||||||
key := p.formatBindKey(kb)
|
key := p.formatBindKey(kb)
|
||||||
|
normalizedKey := normalizeNiriBindKey(key)
|
||||||
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
|
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
|
||||||
|
|
||||||
if isDMSBind {
|
if isDMSBind {
|
||||||
p.dmsBindKeys[key] = true
|
p.dmsBindKeys[normalizedKey] = true
|
||||||
p.dmsBindMap[key] = kb
|
p.dmsBindMap[normalizedKey] = kb
|
||||||
} else if p.dmsBindKeys[key] {
|
} else if p.dmsBindKeys[normalizedKey] {
|
||||||
p.bindsAfterDMS++
|
p.bindsAfterDMS++
|
||||||
p.conflictingConfigs[key] = kb
|
p.conflictingConfigs[normalizedKey] = kb
|
||||||
p.configBindKeys[key] = true
|
p.configBindKeys[normalizedKey] = true
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
p.configBindKeys[key] = true
|
p.configBindKeys[normalizedKey] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := p.bindMap[key]; !exists {
|
if _, exists := p.bindMap[normalizedKey]; !exists {
|
||||||
p.bindOrder = append(p.bindOrder, key)
|
p.bindOrder = append(p.bindOrder, normalizedKey)
|
||||||
}
|
}
|
||||||
p.bindMap[key] = kb
|
p.bindMap[normalizedKey] = kb
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
|
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
|
||||||
|
|||||||
@@ -526,6 +526,50 @@ binds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNiriKeyIdentityIsCaseInsensitive(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("Failed to create dms dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := `binds {
|
||||||
|
Alt+Space hotkey-overlay-title="Spotlight Bar" { spawn "dms" "ipc" "call" "spotlight-bar" "toggle"; }
|
||||||
|
}
|
||||||
|
include "dms/binds.kdl"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
include := `binds {
|
||||||
|
Alt+space hotkey-overlay-title="Default Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "binds.kdl"), []byte(include), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write binds include: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var altSpaceBinds []NiriKeyBinding
|
||||||
|
parser := NewNiriParser("")
|
||||||
|
for _, kb := range result.Section.Keybinds {
|
||||||
|
if normalizeNiriBindKey(parser.formatBindKey(&kb)) == "alt+space" {
|
||||||
|
altSpaceBinds = append(altSpaceBinds, kb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(altSpaceBinds) != 1 {
|
||||||
|
t.Fatalf("Expected one Alt+Space identity, got %d", len(altSpaceBinds))
|
||||||
|
}
|
||||||
|
if got := altSpaceBinds[0].Args; len(got) < 5 || got[3] != "spotlight" || got[4] != "toggle" {
|
||||||
|
t.Fatalf("Expected later DMS include to win with spotlight toggle, got action=%s args=%v", altSpaceBinds[0].Action, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriParseMultipleArgs(t *testing.T) {
|
func TestNiriParseMultipleArgs(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for key, expected := range binds {
|
for key, expected := range binds {
|
||||||
loaded, ok := loadedBinds[key]
|
loaded, ok := loadedBinds[normalizeNiriBindKey(key)]
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Missing bind for key %s", key)
|
t.Errorf("Missing bind for key %s", key)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Keybind struct {
|
|||||||
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
Conflict *Keybind `json:"conflict,omitempty"`
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
|
HasDefault bool `json:"hasDefault,omitempty"` // override has a DMS default to revert to
|
||||||
}
|
}
|
||||||
|
|
||||||
type DMSBindsStatus struct {
|
type DMSBindsStatus struct {
|
||||||
@@ -42,6 +43,11 @@ type Provider interface {
|
|||||||
type WritableProvider interface {
|
type WritableProvider interface {
|
||||||
Provider
|
Provider
|
||||||
SetBind(key, action, description string, options map[string]any) error
|
SetBind(key, action, description string, options map[string]any) error
|
||||||
|
// RemoveBind removes the bind. Hyprland writes a negative override to
|
||||||
|
// dms/binds-user.lua; single-file providers delete the line.
|
||||||
RemoveBind(key string) error
|
RemoveBind(key string) error
|
||||||
|
// ResetBind reverts a user override to its DMS default. On single-file
|
||||||
|
// providers this aliases to RemoveBind.
|
||||||
|
ResetBind(key string) error
|
||||||
GetOverridePath() string
|
GetOverridePath() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package luaconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var luaRequireRE = regexp.MustCompile(`(?i)\brequire\s*\(\s*["']([^"']+)["']\s*\)`)
|
||||||
|
|
||||||
|
func ModuleToRelPath(module string) string {
|
||||||
|
module = strings.TrimSpace(module)
|
||||||
|
if module == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
module = strings.NewReplacer(".", string(filepath.Separator), "/", string(filepath.Separator)).Replace(module)
|
||||||
|
return filepath.Clean(module + ".lua")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleToPath(baseDir, module string) string {
|
||||||
|
rel := ModuleToRelPath(module)
|
||||||
|
if rel == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Clean(filepath.Join(baseDir, rel))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Requires(line string) []string {
|
||||||
|
line = stripLineComment(line)
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
matches := luaRequireRE.FindAllStringSubmatch(line, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
modules := make([]string, 0, len(matches))
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) > 1 && strings.TrimSpace(match[1]) != "" {
|
||||||
|
modules = append(modules, strings.TrimSpace(match[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
func Require(line string) (string, bool) {
|
||||||
|
modules := Requires(line)
|
||||||
|
if len(modules) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return modules[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequiresTarget(filePath, targetAbs string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return requiresTarget(absPath, filepath.Dir(absPath), targetAbs, processed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiresTarget(filePath, rootDir, targetAbs string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
targetAbsClean := filepath.Clean(targetAbs)
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range strings.Split(string(data), "\n") {
|
||||||
|
for _, module := range Requires(raw) {
|
||||||
|
candidate := ModuleToPath(rootDir, module)
|
||||||
|
if candidate == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if filepath.Clean(candidate) == targetAbsClean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||||
|
if requiresTarget(candidate, rootDir, targetAbs, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripLineComment(line string) string {
|
||||||
|
inStr := byte(0)
|
||||||
|
esc := false
|
||||||
|
for i := 0; i+1 < len(line); i++ {
|
||||||
|
c := line[i]
|
||||||
|
if inStr != 0 {
|
||||||
|
if esc {
|
||||||
|
esc = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == '\\' && inStr == '"' {
|
||||||
|
esc = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c == inStr {
|
||||||
|
inStr = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch c {
|
||||||
|
case '"', '\'':
|
||||||
|
inStr = c
|
||||||
|
case '-':
|
||||||
|
if line[i+1] == '-' {
|
||||||
|
return line[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package luaconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModuleToRelPath(t *testing.T) {
|
||||||
|
tests := map[string]string{
|
||||||
|
"dms.binds": filepath.Join("dms", "binds.lua"),
|
||||||
|
"dms/binds-user": filepath.Join("dms", "binds-user.lua"),
|
||||||
|
"awesome/anim": filepath.Join("awesome", "anim.lua"),
|
||||||
|
"awesome.colors": filepath.Join("awesome", "colors.lua"),
|
||||||
|
" awesome.binds ": filepath.Join("awesome", "binds.lua"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for input, want := range tests {
|
||||||
|
if got := ModuleToRelPath(input); got != want {
|
||||||
|
t.Fatalf("ModuleToRelPath(%q) = %q, want %q", input, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiresSkipsComments(t *testing.T) {
|
||||||
|
if modules := Requires(`-- require("dms.binds")`); len(modules) != 0 {
|
||||||
|
t.Fatalf("expected commented require to be ignored, got %#v", modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
modules := Requires(`print("-- not a comment") require("dms.binds") -- require("ignored")`)
|
||||||
|
if len(modules) != 1 || modules[0] != "dms.binds" {
|
||||||
|
t.Fatalf("unexpected modules: %#v", modules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiresTargetRecurses(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
target := filepath.Join(dmsDir, "windowrules.lua")
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`require("dms.extra")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dmsDir, "extra.lua"), []byte(`require("dms.windowrules")`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(target, []byte(`-- rules`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !RequiresTarget(filepath.Join(tmpDir, "hyprland.lua"), target, make(map[string]bool)) {
|
||||||
|
t.Fatal("expected recursive require lookup to find target")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,10 +19,41 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type linkInfo struct {
|
type linkInfo struct {
|
||||||
ifindex int32
|
ifindex int32
|
||||||
name string
|
name string
|
||||||
path dbus.ObjectPath
|
path dbus.ObjectPath
|
||||||
opState string
|
opState string
|
||||||
|
linkType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *linkInfo) isWired() bool {
|
||||||
|
if l.linkType != "" {
|
||||||
|
return l.linkType == "ether"
|
||||||
|
}
|
||||||
|
if looksVirtual(l.name) || strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *linkInfo) isWireless() bool {
|
||||||
|
if l.linkType != "" {
|
||||||
|
return l.linkType == "wlan"
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp")
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksVirtual(name string) bool {
|
||||||
|
virtualPrefixes := []string{
|
||||||
|
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
|
||||||
|
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
|
||||||
|
}
|
||||||
|
for _, prefix := range virtualPrefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemdNetworkdBackend struct {
|
type SystemdNetworkdBackend struct {
|
||||||
@@ -95,17 +127,50 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
|
|||||||
defer b.linksMutex.Unlock()
|
defer b.linksMutex.Unlock()
|
||||||
|
|
||||||
for _, l := range links {
|
for _, l := range links {
|
||||||
b.links[l.Name] = &linkInfo{
|
if existing, ok := b.links[l.Name]; ok && existing.path == l.Path {
|
||||||
ifindex: l.Ifindex,
|
existing.ifindex = l.Ifindex
|
||||||
name: l.Name,
|
continue
|
||||||
path: l.Path,
|
|
||||||
}
|
}
|
||||||
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s)", l.Name, l.Ifindex, l.Path)
|
info := &linkInfo{
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchLinkType queries networkd's Describe method and extracts the link Type
|
||||||
|
// (e.g. "ether", "wlan", "loopback", "none"). Returns empty on failure; callers
|
||||||
|
// fall back to name-prefix heuristics in that case. The Type is fixed at link
|
||||||
|
// creation by the kernel, so callers cache the result for the lifetime of the
|
||||||
|
// linkInfo and only refetch when a link is re-created at a new D-Bus path.
|
||||||
|
func (b *SystemdNetworkdBackend) fetchLinkType(path dbus.ObjectPath) string {
|
||||||
|
linkObj := b.conn.Object(networkdBusName, path)
|
||||||
|
var describeJSON string
|
||||||
|
if err := linkObj.Call(networkdLinkIface+".Describe", 0).Store(&describeJSON); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parseDescribeType(describeJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDescribeType extracts the top-level "Type" field from a networkd
|
||||||
|
// Describe payload. Returns empty when the JSON is malformed or the field is
|
||||||
|
// absent, signalling callers to fall back to name-prefix heuristics.
|
||||||
|
func parseDescribeType(describeJSON string) string {
|
||||||
|
var parsed struct {
|
||||||
|
Type string `json:"Type"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(describeJSON), &parsed); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parsed.Type
|
||||||
|
}
|
||||||
|
|
||||||
func (b *SystemdNetworkdBackend) updateState() error {
|
func (b *SystemdNetworkdBackend) updateState() error {
|
||||||
b.linksMutex.RLock()
|
b.linksMutex.RLock()
|
||||||
defer b.linksMutex.RUnlock()
|
defer b.linksMutex.RUnlock()
|
||||||
@@ -113,8 +178,8 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
|||||||
var wiredIface *linkInfo
|
var wiredIface *linkInfo
|
||||||
var wifiIface *linkInfo
|
var wifiIface *linkInfo
|
||||||
|
|
||||||
for name, link := range b.links {
|
for _, link := range b.links {
|
||||||
if b.isVirtualInterface(name) {
|
if !link.isWired() && !link.isWireless() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +191,11 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
if link.isWireless() {
|
||||||
if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||||
wifiIface = link
|
wifiIface = link
|
||||||
}
|
}
|
||||||
} else if !b.isVirtualInterface(name) {
|
} else if link.isWired() {
|
||||||
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
|
||||||
wiredIface = link
|
wiredIface = link
|
||||||
}
|
}
|
||||||
@@ -140,7 +205,7 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
|||||||
var wiredConns []WiredConnection
|
var wiredConns []WiredConnection
|
||||||
var ethernetDevices []EthernetDevice
|
var ethernetDevices []EthernetDevice
|
||||||
for name, link := range b.links {
|
for name, link := range b.links {
|
||||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
if !link.isWired() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,19 +294,6 @@ func (b *SystemdNetworkdBackend) updateState() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SystemdNetworkdBackend) isVirtualInterface(name string) bool {
|
|
||||||
virtualPrefixes := []string{
|
|
||||||
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
|
|
||||||
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
|
|
||||||
}
|
|
||||||
for _, prefix := range virtualPrefixes {
|
|
||||||
if strings.HasPrefix(name, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string {
|
func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string {
|
||||||
iface, err := net.InterfaceByName(ifname)
|
iface, err := net.InterfaceByName(ifname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error
|
|||||||
|
|
||||||
var conns []WiredConnection
|
var conns []WiredConnection
|
||||||
for name, link := range b.links {
|
for name, link := range b.links {
|
||||||
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
if !link.isWired() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetwor
|
|||||||
func (b *SystemdNetworkdBackend) ConnectEthernet() error {
|
func (b *SystemdNetworkdBackend) ConnectEthernet() error {
|
||||||
b.linksMutex.RLock()
|
b.linksMutex.RLock()
|
||||||
var primaryWired *linkInfo
|
var primaryWired *linkInfo
|
||||||
for name, l := range b.links {
|
for _, l := range b.links {
|
||||||
if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
|
if !l.isWired() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
primaryWired = l
|
primaryWired = l
|
||||||
|
|||||||
@@ -145,3 +145,73 @@ func TestSystemdNetworkdBackend_DisconnectEthernetDevice(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "not supported")
|
assert.Contains(t, err.Error(), "not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLinkInfo_Classify(t *testing.T) {
|
||||||
|
// When networkd reports a Type via Describe, classification is exact.
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
ifname string
|
||||||
|
linkType string
|
||||||
|
wantWired bool
|
||||||
|
wantWifi bool
|
||||||
|
}{
|
||||||
|
{"ether type", "dock", "ether", true, false},
|
||||||
|
{"wlan type", "wifi", "wlan", false, true},
|
||||||
|
{"loopback type", "lo", "loopback", false, false},
|
||||||
|
{"none type (tun overlay)", "nebula.homelab", "none", false, false},
|
||||||
|
{"none type (wireguard)", "wg0", "none", false, false},
|
||||||
|
// Fallback path: linkType unavailable, name-prefix heuristic applies.
|
||||||
|
{"fallback enp wired", "enp141s0", "", true, false},
|
||||||
|
{"fallback wlan wireless", "wlan0", "", false, true},
|
||||||
|
{"fallback wlp wireless", "wlp3s0", "", false, true},
|
||||||
|
{"fallback lo skipped", "lo", "", false, false},
|
||||||
|
{"fallback docker skipped", "docker0", "", false, false},
|
||||||
|
{"fallback tun skipped", "tun0", "", false, false},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
l := &linkInfo{name: tc.ifname, linkType: tc.linkType}
|
||||||
|
assert.Equal(t, tc.wantWired, l.isWired(), "isWired")
|
||||||
|
assert.Equal(t, tc.wantWifi, l.isWireless(), "isWireless")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDescribeType(t *testing.T) {
|
||||||
|
// parseDescribeType is the seam between networkd's Describe RPC and the
|
||||||
|
// classifier. On any failure path it must return "" so callers fall back
|
||||||
|
// to name-prefix heuristics rather than misclassifying the link.
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"ether", `{"Type":"ether","Name":"enp141s0"}`, "ether"},
|
||||||
|
{"wlan", `{"Type":"wlan","Name":"wlan0"}`, "wlan"},
|
||||||
|
{"loopback", `{"Type":"loopback","Name":"lo"}`, "loopback"},
|
||||||
|
{"none with kind", `{"Type":"none","Kind":"tun","Name":"nebula.homelab"}`, "none"},
|
||||||
|
{"empty payload", ``, ""},
|
||||||
|
{"empty object", `{}`, ""},
|
||||||
|
{"missing Type field", `{"Name":"wlan0","Kind":""}`, ""},
|
||||||
|
{"explicit empty Type", `{"Type":"","Name":"wlan0"}`, ""},
|
||||||
|
{"malformed json", `{"Type":"ether"`, ""},
|
||||||
|
{"non-string Type", `{"Type":42}`, ""},
|
||||||
|
{"unrelated payload", `"just a string"`, ""},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.want, parseDescribeType(tc.in))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"}
|
||||||
|
for _, n := range virtual {
|
||||||
|
assert.True(t, looksVirtual(n), "%s should look virtual", n)
|
||||||
|
}
|
||||||
|
real := []string{"enp141s0", "eno1", "wlan0", "wlp3s0", "wifi", "dock", "nebula.homelab", "wg0"}
|
||||||
|
for _, n := range real {
|
||||||
|
assert.False(t, looksVirtual(n), "%s should not look virtual", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -300,9 +300,14 @@ func (m Model) checkExistingConfigurations() tea.Cmd {
|
|||||||
Exists: niriExists,
|
Exists: niriExists,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
hyprlandPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
hyprlandLuaPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.lua")
|
||||||
|
hyprlandConfPath := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf")
|
||||||
|
hyprlandPath := hyprlandLuaPath
|
||||||
hyprlandExists := false
|
hyprlandExists := false
|
||||||
if _, err := os.Stat(hyprlandPath); err == nil {
|
if _, err := os.Stat(hyprlandLuaPath); err == nil {
|
||||||
|
hyprlandExists = true
|
||||||
|
} else if _, err := os.Stat(hyprlandConfPath); err == nil {
|
||||||
|
hyprlandPath = hyprlandConfPath
|
||||||
hyprlandExists = true
|
hyprlandExists = true
|
||||||
}
|
}
|
||||||
configs = append(configs, ExistingConfigInfo{
|
configs = append(configs, ExistingConfigInfo{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseWindowRuleV1(t *testing.T) {
|
func TestParseWindowRuleV1(t *testing.T) {
|
||||||
@@ -151,7 +154,7 @@ func TestHyprlandWritableProvider(t *testing.T) {
|
|||||||
t.Errorf("Name() = %q, want hyprland", provider.Name())
|
t.Errorf("Name() = %q, want hyprland", provider.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.conf")
|
expectedPath := filepath.Join(tmpDir, "dms", "windowrules.lua")
|
||||||
if provider.GetOverridePath() != expectedPath {
|
if provider.GetOverridePath() != expectedPath {
|
||||||
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath)
|
||||||
}
|
}
|
||||||
@@ -270,6 +273,104 @@ windowrulev2 = tile, class:^(extraapp)$
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseHyprlandLuaRequiresFragment(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLua := filepath.Join(tmpDir, "hyprland.lua")
|
||||||
|
fragLua := filepath.Join(dmsDir, "windowrules.lua")
|
||||||
|
|
||||||
|
if err := os.WriteFile(fragLua, []byte(`
|
||||||
|
hl.window_rule({ match = { class = "^test$" }, float = true })
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(mainLua, []byte(`
|
||||||
|
require("dms.windowrules")
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ParseHyprlandWindowRules(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseHyprlandWindowRules: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(res.Rules))
|
||||||
|
}
|
||||||
|
if !res.DMSRulesIncluded {
|
||||||
|
t.Fatal("expected dms.windowrules fragment to be marked included")
|
||||||
|
}
|
||||||
|
wr := ConvertHyprlandRulesToWindowRules(res.Rules)[0]
|
||||||
|
if wr.MatchCriteria.AppID != "^test$" || wr.Actions.OpenFloating == nil || !*wr.Actions.OpenFloating {
|
||||||
|
t.Fatalf("unexpected merged rule: %#v", wr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHyprlandLuaNoInitialFocusAlias(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.lua"), []byte(`
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^steam$" },
|
||||||
|
no_initial_focus = true,
|
||||||
|
})
|
||||||
|
`), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ParseHyprlandWindowRules(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseHyprlandWindowRules: %v", err)
|
||||||
|
}
|
||||||
|
if len(res.Rules) != 1 {
|
||||||
|
t.Fatalf("expected 1 rule, got %d", len(res.Rules))
|
||||||
|
}
|
||||||
|
wr := ConvertHyprlandRulesToWindowRules(res.Rules)[0]
|
||||||
|
if wr.Actions.NoFocus == nil || !*wr.Actions.NoFocus {
|
||||||
|
t.Fatalf("expected no_initial_focus to populate NoFocus action: %#v", wr.Actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatLuaManagedHyprRuleUsesLuaFieldNames(t *testing.T) {
|
||||||
|
enabled := true
|
||||||
|
rule := windowrules.WindowRule{
|
||||||
|
ID: "test-rule",
|
||||||
|
Enabled: true,
|
||||||
|
MatchCriteria: windowrules.MatchCriteria{
|
||||||
|
AppID: "^app$",
|
||||||
|
},
|
||||||
|
Actions: windowrules.Actions{
|
||||||
|
NoFocus: &enabled,
|
||||||
|
NoShadow: &enabled,
|
||||||
|
NoDim: &enabled,
|
||||||
|
NoBlur: &enabled,
|
||||||
|
NoAnim: &enabled,
|
||||||
|
ForcergbX: &enabled,
|
||||||
|
Idleinhibit: "focus",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := formatLuaManagedHyprRule(rule)
|
||||||
|
joined := strings.Join(lines, "\n")
|
||||||
|
for _, want := range []string{
|
||||||
|
"no_focus = true",
|
||||||
|
"no_shadow = true",
|
||||||
|
"no_dim = true",
|
||||||
|
"no_blur = true",
|
||||||
|
"no_anim = true",
|
||||||
|
"force_rgbx = true",
|
||||||
|
`idle_inhibit = "focus"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(joined, want) {
|
||||||
|
t.Fatalf("formatted rule missing %q: %s", want, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoolToInt(t *testing.T) {
|
func TestBoolToInt(t *testing.T) {
|
||||||
if boolToInt(true) != 1 {
|
if boolToInt(true) != 1 {
|
||||||
t.Error("boolToInt(true) should be 1")
|
t.Error("boolToInt(true) should be 1")
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ override_dh_auto_install:
|
|||||||
install -Dm644 $$SOURCE_DIR/LICENSE \
|
install -Dm644 $$SOURCE_DIR/LICENSE \
|
||||||
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
|
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
|
||||||
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
|
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
|
||||||
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
|
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf && \
|
||||||
|
install -Dm644 $$SOURCE_DIR/systemd/sysusers-dms-greeter.conf \
|
||||||
|
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
|
||||||
else \
|
else \
|
||||||
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
|
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
|
||||||
echo "Contents of current directory:" && ls -la && exit 1; \
|
echo "Contents of current directory:" && ls -la && exit 1; \
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docd
|
|||||||
|
|
||||||
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
|
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
|
||||||
|
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
||||||
@@ -78,6 +80,7 @@ fi
|
|||||||
%{_bindir}/dms-greeter
|
%{_bindir}/dms-greeter
|
||||||
%{_datadir}/quickshell/dms-greeter/
|
%{_datadir}/quickshell/dms-greeter/
|
||||||
%{_tmpfilesdir}/%{name}.conf
|
%{_tmpfilesdir}/%{name}.conf
|
||||||
|
%{_sysusersdir}/dms-greeter.conf
|
||||||
|
|
||||||
%pre
|
%pre
|
||||||
# Create greeter user/group if they don't exist
|
# Create greeter user/group if they don't exist
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docd
|
|||||||
|
|
||||||
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
|
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
|
||||||
|
|
||||||
|
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
|
||||||
@@ -78,6 +80,7 @@ fi
|
|||||||
%dir %{_datadir}/quickshell
|
%dir %{_datadir}/quickshell
|
||||||
%{_datadir}/quickshell/dms-greeter/
|
%{_datadir}/quickshell/dms-greeter/
|
||||||
%{_tmpfilesdir}/%{name}.conf
|
%{_tmpfilesdir}/%{name}.conf
|
||||||
|
%{_sysusersdir}/dms-greeter.conf
|
||||||
|
|
||||||
%pre
|
%pre
|
||||||
# Create greeter user/group if they don't exist
|
# Create greeter user/group if they don't exist
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ override_dh_auto_install:
|
|||||||
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
|
||||||
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
|
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
|
||||||
|
|
||||||
|
install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
|
||||||
|
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf
|
||||||
|
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
|
||||||
|
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf
|
||||||
|
|
||||||
# Create cache directory structure (will be created by postinst)
|
# Create cache directory structure (will be created by postinst)
|
||||||
mkdir -p debian/dms-greeter/var/cache/dms-greeter
|
mkdir -p debian/dms-greeter/var/cache/dms-greeter
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Hyprland Lua Migration
|
||||||
|
|
||||||
|
Hyprland 0.55 moved configuration toward Lua. DMS now follows that path for new
|
||||||
|
Hyprland setup and migration.
|
||||||
|
|
||||||
|
This guide covers what changes, where files live, and how to check that your
|
||||||
|
session is using the new config.
|
||||||
|
|
||||||
|
## Quick Summary
|
||||||
|
|
||||||
|
DMS now deploys Hyprland as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/hyprland.lua
|
||||||
|
~/.config/hypr/dms/*.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
The old hyprlang files are moved out of the active config tree:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/hyprland.conf
|
||||||
|
~/.config/hypr/dms/*.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups are stored here:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/.dms-backups/<timestamp>/
|
||||||
|
```
|
||||||
|
|
||||||
|
## What `dms setup` Does
|
||||||
|
|
||||||
|
When Hyprland is selected, `dms setup` writes a Lua main config and DMS Lua
|
||||||
|
fragments.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `hyprland.lua` | Main Hyprland config. |
|
||||||
|
| `dms/colors.lua` | Theme colors. |
|
||||||
|
| `dms/outputs.lua` | Monitors and display settings. |
|
||||||
|
| `dms/layout.lua` | Layout, gaps, borders, and decoration. |
|
||||||
|
| `dms/cursor.lua` | Cursor settings. |
|
||||||
|
| `dms/binds.lua` | DMS-managed default shortcuts. |
|
||||||
|
| `dms/binds-user.lua` | User shortcut overrides. |
|
||||||
|
| `dms/windowrules.lua` | Window rules. |
|
||||||
|
|
||||||
|
`dms/binds.lua` is managed by DMS and may be refreshed by setup. Put custom
|
||||||
|
keyboard shortcuts in `dms/binds-user.lua`, or use the Keyboard Shortcuts page in
|
||||||
|
DMS Settings.
|
||||||
|
|
||||||
|
Most other existing non-empty Lua fragments are preserved.
|
||||||
|
|
||||||
|
## Legacy Config Migration
|
||||||
|
|
||||||
|
During migration, DMS moves legacy active files into the backup folder so
|
||||||
|
Hyprland does not see both config formats at once.
|
||||||
|
|
||||||
|
DMS also migrates legacy `monitor = ...` lines from `hyprland.conf` into
|
||||||
|
`dms/outputs.lua` when `outputs.lua` is empty or missing. If you already have a
|
||||||
|
custom `outputs.lua`, DMS leaves it alone.
|
||||||
|
|
||||||
|
## DMS Settings Support
|
||||||
|
|
||||||
|
DMS Settings now targets Lua files for Hyprland:
|
||||||
|
|
||||||
|
| Settings page | Lua file |
|
||||||
|
| --- | --- |
|
||||||
|
| Keyboard Shortcuts | `dms/binds-user.lua` |
|
||||||
|
| Displays | `dms/outputs.lua` |
|
||||||
|
| Theme Colors | `dms/colors.lua` |
|
||||||
|
| Cursor | `dms/cursor.lua` |
|
||||||
|
| Window Rules | `dms/windowrules.lua` |
|
||||||
|
|
||||||
|
The main config should include the DMS fragments:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("dms.colors")
|
||||||
|
require("dms.outputs")
|
||||||
|
require("dms.layout")
|
||||||
|
require("dms.cursor")
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
require("dms.windowrules")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Shortcuts: Delete and Reset
|
||||||
|
|
||||||
|
The Keyboard Shortcuts page exposes two actions on any DMS-managed bind:
|
||||||
|
|
||||||
|
- **Delete** — removes the shortcut entirely. For default DMS shortcuts (from
|
||||||
|
`dms/binds.lua`), this saves an `hl.unbind("KEY")` line into
|
||||||
|
`dms/binds-user.lua` so the removal sticks across `dms setup` runs.
|
||||||
|
- **Reset to default** — only visible when you are editing a user override of
|
||||||
|
a DMS default. It drops your override so the original DMS default re-applies.
|
||||||
|
|
||||||
|
Binds from your own `hyprland.lua` (outside the `dms/` folder) are read-only
|
||||||
|
in Settings — DMS does not write into files it does not manage.
|
||||||
|
|
||||||
|
## Starting Hyprland
|
||||||
|
|
||||||
|
For the Lua config to be active, Hyprland must start with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Hyprland -c ~/.config/hypr/hyprland.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
If Hyprland warns that it is using an autogenerated config, or the warning
|
||||||
|
mentions `hyprland.conf`, the session is not using the DMS Lua config yet.
|
||||||
|
|
||||||
|
## Verify Everything
|
||||||
|
|
||||||
|
After updating DMS, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dms setup
|
||||||
|
hyprctl reload
|
||||||
|
hyprctl configerrors
|
||||||
|
```
|
||||||
|
|
||||||
|
If the current session was not started from `hyprland.lua`, restart Hyprland with
|
||||||
|
the Lua config and check again.
|
||||||
|
|
||||||
|
Useful file checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
test -f ~/.config/hypr/hyprland.lua
|
||||||
|
test ! -f ~/.config/hypr/hyprland.conf
|
||||||
|
ls ~/.config/hypr/dms
|
||||||
|
```
|
||||||
|
|
||||||
|
The live `dms` folder should contain Lua files like `binds.lua`,
|
||||||
|
`binds-user.lua`, `outputs.lua`, and `windowrules.lua`.
|
||||||
|
|
||||||
|
Note: Hyprland 0.55 still auto-generates `hyprland.conf` if you launch it
|
||||||
|
without `-c ~/.config/hypr/hyprland.lua`. DMS sweeps any stray
|
||||||
|
`hyprland.conf` into `.dms-backups/<timestamp>/` on the next `dms run`
|
||||||
|
startup, so the second check above is the right long-term state. If you see
|
||||||
|
`hyprland.conf` persist between `dms run` invocations, the session was not
|
||||||
|
started from `hyprland.lua` — restart Hyprland with the `-c` flag (or update
|
||||||
|
your session/desktop entry to include it).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If shortcuts do not work, confirm `hyprland.lua` includes both:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require("dms.binds")
|
||||||
|
require("dms.binds-user")
|
||||||
|
```
|
||||||
|
|
||||||
|
If `hyprctl configerrors` reports errors in `dms/binds.lua`, rerun `dms setup`
|
||||||
|
with the latest DMS binary so the DMS-managed shortcut file is refreshed.
|
||||||
|
|
||||||
|
If a migrated monitor setup looks wrong, compare:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/dms/outputs.lua
|
||||||
|
~/.config/hypr/.dms-backups/<timestamp>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Your previous config should be available in the timestamped backup folder.
|
||||||
|
|
||||||
|
## Reference Map
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/hypr/
|
||||||
|
|-- hyprland.lua # Main DMS Hyprland config
|
||||||
|
|-- .dms-backups/ # Timestamped backups from setup/migration
|
||||||
|
`-- dms/
|
||||||
|
|-- colors.lua # Theme colors
|
||||||
|
|-- outputs.lua # Monitor/output config
|
||||||
|
|-- layout.lua # Layout, gaps, borders, decoration
|
||||||
|
|-- cursor.lua # Cursor settings
|
||||||
|
|-- binds.lua # DMS-managed default shortcuts
|
||||||
|
|-- binds-user.lua # User shortcut overrides
|
||||||
|
`-- windowrules.lua # DMS-managed window rules
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy files such as `hyprland.conf` and `dms/*.conf` should live in
|
||||||
|
`.dms-backups/<timestamp>/` after migration, not in the active config tree.
|
||||||
|
|
||||||
|
## Maintainer Note
|
||||||
|
|
||||||
|
Embedded source files live in `core/internal/config/embedded/` and use names like
|
||||||
|
`hypr-binds.lua`. Installed user files use shorter names like `dms/binds.lua`.
|
||||||
|
|
||||||
|
After changing Hyprland config deployment or parsing, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd core
|
||||||
|
go test ./internal/config ./internal/keybinds/providers ./internal/windowrules/providers
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
+46
@@ -212,6 +212,52 @@ dms ipc call lock lock
|
|||||||
dms ipc call lock isLocked
|
dms ipc call lock isLocked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Target: `sessions`
|
||||||
|
|
||||||
|
Logind session enumeration and seat-local session switching. Wraps `loginctl list-sessions` and `loginctl activate`. Only switches between sessions that are *already running* on the current seat — creating a fresh login as another user requires a multi-session greeter setup (greetd-flexiserver / GDM / LightDM) and is out of scope.
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
**`list`**
|
||||||
|
- Print every session DMS knows about as tab-separated columns: `sessionId\tusername\tseat\ttty\ttype\tcurrent-marker`
|
||||||
|
- Returns: Multi-line string. The current session is marked with `*current*`.
|
||||||
|
|
||||||
|
**`refresh`**
|
||||||
|
- Re-enumerate sessions in the background (the picker also refreshes itself on open)
|
||||||
|
- Returns: `"ok"`
|
||||||
|
|
||||||
|
**`open`**
|
||||||
|
- Refresh and open the Switch User picker on the focused screen
|
||||||
|
- Returns: `"ok"`
|
||||||
|
|
||||||
|
**`activate <sessionId>`**
|
||||||
|
- Activate a session by its numeric logind ID (the `Id=` field from `loginctl show-session`). Performs a VT switch
|
||||||
|
- Parameters: `sessionId` - Numeric session ID
|
||||||
|
- Returns: `"ok"` on dispatch, `"ERROR: missing session id"` if blank
|
||||||
|
- Note: Failures from `loginctl activate` surface through the `switchFailed` QML signal and a Log warning — the IPC call returns success once the spawn is queued, not after activation completes
|
||||||
|
|
||||||
|
**`switchTo <target>`**
|
||||||
|
- Switch to another session by username *or* session ID. The first non-current session matching the username wins; if there's no match, the call fails through the same logging path as `activate`
|
||||||
|
- Parameters: `target` - Username (e.g. `testuser2`) or numeric session ID
|
||||||
|
- Returns: `"ok"` on dispatch, `"ERROR: missing target (username or session id)"` if blank
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```bash
|
||||||
|
# Inspect what's switchable
|
||||||
|
dms ipc call sessions list
|
||||||
|
|
||||||
|
# Open the picker (useful for a keybind)
|
||||||
|
dms ipc call sessions open
|
||||||
|
|
||||||
|
# Jump straight to another logged-in user without the picker
|
||||||
|
dms ipc call sessions switchTo testuser2
|
||||||
|
|
||||||
|
# Or by session ID, when the user has multiple sessions
|
||||||
|
dms ipc call sessions activate 4
|
||||||
|
```
|
||||||
|
|
||||||
|
The dedicated `dms switch-user [target]` CLI command wraps the same behavior with a friendlier error path (it prints the switchable list when no target matches).
|
||||||
|
|
||||||
## Target: `inhibit`
|
## Target: `inhibit`
|
||||||
|
|
||||||
Idle inhibitor control to prevent automatic sleep/lock.
|
Idle inhibitor control to prevent automatic sleep/lock.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
function shQuote(value) {
|
||||||
|
return "'" + String(value ?? "").replace(/'/g, "'\\''") + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirname(path) {
|
||||||
|
const idx = String(path ?? "").lastIndexOf("/");
|
||||||
|
return idx > 0 ? path.substring(0, idx) : ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRepairScript(options) {
|
||||||
|
const configFile = options.configFile;
|
||||||
|
const backupFile = options.backupFile;
|
||||||
|
const fragments = options.fragmentFiles || (options.fragmentFile ? [options.fragmentFile] : []);
|
||||||
|
const includes = options.includes || [{
|
||||||
|
grepPattern: options.grepPattern,
|
||||||
|
includeLine: options.includeLine
|
||||||
|
}];
|
||||||
|
|
||||||
|
const commands = [];
|
||||||
|
if (backupFile)
|
||||||
|
commands.push(`cp ${shQuote(configFile)} ${shQuote(backupFile)} 2>/dev/null || true`);
|
||||||
|
|
||||||
|
const dirs = {};
|
||||||
|
for (const fragment of fragments)
|
||||||
|
dirs[dirname(fragment)] = true;
|
||||||
|
for (const dir in dirs)
|
||||||
|
commands.push(`mkdir -p ${shQuote(dir)}`);
|
||||||
|
if (fragments.length > 0)
|
||||||
|
commands.push("touch " + fragments.map(shQuote).join(" "));
|
||||||
|
|
||||||
|
for (const include of includes) {
|
||||||
|
if (!include.grepPattern || !include.includeLine)
|
||||||
|
continue;
|
||||||
|
commands.push(`if ! grep -v '^[[:space:]]*\\(//\\|#\\|--\\)' ${shQuote(configFile)} 2>/dev/null | grep -q ${shQuote(include.grepPattern)}; then echo '' >> ${shQuote(configFile)} && printf '%s\\n' ${shQuote(include.includeLine)} >> ${shQuote(configFile)}; fi`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands.join("; ");
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@ const ACTION_TYPES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DMS_ACTIONS = [
|
const DMS_ACTIONS = [
|
||||||
{ id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" },
|
{ id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" },
|
||||||
{ id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" },
|
{ id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" },
|
||||||
{ id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" },
|
{ id: "spawn dms ipc call spotlight close", label: "Default Launcher: Close" },
|
||||||
|
{ id: "spawn dms ipc call spotlight-bar toggle", label: "Spotlight Bar: Toggle" },
|
||||||
|
{ id: "spawn dms ipc call spotlight-bar open", label: "Spotlight Bar: Open" },
|
||||||
|
{ id: "spawn dms ipc call spotlight-bar close", label: "Spotlight Bar: Close" },
|
||||||
{ id: "spawn dms ipc call clipboard toggle", label: "Clipboard: Toggle" },
|
{ id: "spawn dms ipc call clipboard toggle", label: "Clipboard: Toggle" },
|
||||||
{ id: "spawn dms ipc call clipboard open", label: "Clipboard: Open" },
|
{ id: "spawn dms ipc call clipboard open", label: "Clipboard: Open" },
|
||||||
{ id: "spawn dms ipc call clipboard close", label: "Clipboard: Close" },
|
{ id: "spawn dms ipc call clipboard close", label: "Clipboard: Close" },
|
||||||
@@ -63,7 +66,7 @@ const DMS_ACTIONS = [
|
|||||||
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
|
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
|
||||||
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
|
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
|
||||||
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
|
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
|
||||||
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" },
|
{ id: "spawn dms ipc call mic mute", label: "Microphone Mute Toggle" },
|
||||||
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
|
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
|
||||||
{ id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
|
{ id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
|
||||||
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
|
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ Singleton {
|
|||||||
|
|
||||||
property var currentOSDsByScreen: ({})
|
property var currentOSDsByScreen: ({})
|
||||||
|
|
||||||
Connections {
|
Timer {
|
||||||
target: Quickshell
|
id: screensChangedDelayTimer
|
||||||
function onScreensChanged() {
|
interval: 3000 // 3 seconds
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
const activeNames = {};
|
const activeNames = {};
|
||||||
for (let i = 0; i < Quickshell.screens.length; i++)
|
for (let i = 0; i < Quickshell.screens.length; i++)
|
||||||
activeNames[Quickshell.screens[i].name] = true;
|
activeNames[Quickshell.screens[i].name] = true;
|
||||||
@@ -22,6 +24,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
screensChangedDelayTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showOSD(osd) {
|
function showOSD(osd) {
|
||||||
if (!osd || !osd.screen)
|
if (!osd || !osd.screen)
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ Singleton {
|
|||||||
property string timeLocale: ""
|
property string timeLocale: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
|
property string launcherLastFileSearchType: "all"
|
||||||
property string launcherLastQuery: ""
|
property string launcherLastQuery: ""
|
||||||
property var launcherQueryHistory: []
|
property var launcherQueryHistory: []
|
||||||
property string appDrawerLastMode: "apps"
|
property string appDrawerLastMode: "apps"
|
||||||
@@ -1178,6 +1179,17 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLauncherRestoreMode() {
|
||||||
|
if (!SettingsData.rememberLastMode)
|
||||||
|
return "all";
|
||||||
|
return launcherLastMode || "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLauncherLastFileSearchType(type) {
|
||||||
|
launcherLastFileSearchType = type;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
function setLauncherLastQuery(query) {
|
function setLauncherLastQuery(query) {
|
||||||
launcherLastQuery = query;
|
launcherLastQuery = query;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -258,8 +258,6 @@ Singleton {
|
|||||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
onFrameLauncherEmergeSideChanged: saveSettings()
|
||||||
property bool frameLauncherArcExtender: false
|
property bool frameLauncherArcExtender: false
|
||||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
onFrameLauncherArcExtenderChanged: saveSettings()
|
||||||
property bool frameUseSpotlightLauncher: false
|
|
||||||
onFrameUseSpotlightLauncherChanged: saveSettings()
|
|
||||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
||||||
property string frameMode: "connected"
|
property string frameMode: "connected"
|
||||||
onFrameModeChanged: saveSettings()
|
onFrameModeChanged: saveSettings()
|
||||||
@@ -394,6 +392,7 @@ Singleton {
|
|||||||
property string audioScrollMode: "volume"
|
property string audioScrollMode: "volume"
|
||||||
property int audioWheelScrollAmount: 5
|
property int audioWheelScrollAmount: 5
|
||||||
property bool clockCompactMode: false
|
property bool clockCompactMode: false
|
||||||
|
property int focusedWindowSize: 1
|
||||||
property bool focusedWindowCompactMode: false
|
property bool focusedWindowCompactMode: false
|
||||||
property bool runningAppsCompactMode: true
|
property bool runningAppsCompactMode: true
|
||||||
property int barMaxVisibleApps: 0
|
property int barMaxVisibleApps: 0
|
||||||
@@ -436,6 +435,7 @@ Singleton {
|
|||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
property bool rememberLastQuery: false
|
property bool rememberLastQuery: false
|
||||||
|
property bool rememberLastMode: true
|
||||||
property var spotlightSectionViewModes: ({})
|
property var spotlightSectionViewModes: ({})
|
||||||
onSpotlightSectionViewModesChanged: saveSettings()
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
property var appDrawerSectionViewModes: ({})
|
property var appDrawerSectionViewModes: ({})
|
||||||
@@ -449,7 +449,9 @@ Singleton {
|
|||||||
property bool dankLauncherV2UnloadOnClose: false
|
property bool dankLauncherV2UnloadOnClose: false
|
||||||
property bool dankLauncherV2IncludeFilesInAll: false
|
property bool dankLauncherV2IncludeFilesInAll: false
|
||||||
property bool dankLauncherV2IncludeFoldersInAll: false
|
property bool dankLauncherV2IncludeFoldersInAll: false
|
||||||
|
property bool launcherUseOverlayLayer: false
|
||||||
property string launcherStyle: "full"
|
property string launcherStyle: "full"
|
||||||
|
property bool spotlightBarShowModeChips: false
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
@@ -606,7 +608,7 @@ Singleton {
|
|||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
property bool dockSmartAutoHide: false
|
property bool dockSmartAutoHide: false
|
||||||
property bool dockHideOnFullscreen: true
|
property bool dockUseOverlayLayer: false
|
||||||
property bool dockGroupByApp: false
|
property bool dockGroupByApp: false
|
||||||
property bool dockRestoreSpecialWorkspaceOnClick: false
|
property bool dockRestoreSpecialWorkspaceOnClick: false
|
||||||
property bool dockOpenOnOverview: false
|
property bool dockOpenOnOverview: false
|
||||||
@@ -686,6 +688,7 @@ Singleton {
|
|||||||
property int notificationTimeoutNormal: 5000
|
property int notificationTimeoutNormal: 5000
|
||||||
property int notificationTimeoutCritical: 0
|
property int notificationTimeoutCritical: 0
|
||||||
property bool notificationCompactMode: false
|
property bool notificationCompactMode: false
|
||||||
|
property bool notificationDedupeEnabled: true
|
||||||
property int notificationPopupPosition: SettingsData.Position.Top
|
property int notificationPopupPosition: SettingsData.Position.Top
|
||||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||||
property int notificationCustomAnimationDuration: 400
|
property int notificationCustomAnimationDuration: 400
|
||||||
@@ -706,6 +709,7 @@ Singleton {
|
|||||||
property bool osdBrightnessEnabled: true
|
property bool osdBrightnessEnabled: true
|
||||||
property bool osdIdleInhibitorEnabled: true
|
property bool osdIdleInhibitorEnabled: true
|
||||||
property bool osdMicMuteEnabled: true
|
property bool osdMicMuteEnabled: true
|
||||||
|
property bool osdMicVolumeEnabled: true
|
||||||
property bool osdCapsLockEnabled: true
|
property bool osdCapsLockEnabled: true
|
||||||
property bool osdPowerProfileEnabled: true
|
property bool osdPowerProfileEnabled: true
|
||||||
property bool osdAudioOutputEnabled: true
|
property bool osdAudioOutputEnabled: true
|
||||||
@@ -787,6 +791,7 @@ Singleton {
|
|||||||
"popupGapsAuto": true,
|
"popupGapsAuto": true,
|
||||||
"popupGapsManual": 4,
|
"popupGapsManual": 4,
|
||||||
"maximizeDetection": true,
|
"maximizeDetection": true,
|
||||||
|
"useOverlayLayer": false,
|
||||||
"scrollEnabled": true,
|
"scrollEnabled": true,
|
||||||
"scrollXBehavior": "column",
|
"scrollXBehavior": "column",
|
||||||
"scrollYBehavior": "workspace",
|
"scrollYBehavior": "workspace",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ var SPEC = {
|
|||||||
timeLocale: { def: "" },
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
|
launcherLastFileSearchType: { def: "all" },
|
||||||
launcherLastQuery: { def: "" },
|
launcherLastQuery: { def: "" },
|
||||||
launcherQueryHistory: { def: [] },
|
launcherQueryHistory: { def: [] },
|
||||||
appDrawerLastMode: { def: "apps" },
|
appDrawerLastMode: { def: "apps" },
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ var SPEC = {
|
|||||||
audioWheelScrollAmount: { def: 5 },
|
audioWheelScrollAmount: { def: 5 },
|
||||||
clockCompactMode: { def: false },
|
clockCompactMode: { def: false },
|
||||||
focusedWindowCompactMode: { def: false },
|
focusedWindowCompactMode: { def: false },
|
||||||
|
focusedWindowSize: { def: 1 },
|
||||||
runningAppsCompactMode: { def: true },
|
runningAppsCompactMode: { def: true },
|
||||||
barMaxVisibleApps: { def: 0 },
|
barMaxVisibleApps: { def: 0 },
|
||||||
barMaxVisibleRunningApps: { def: 0 },
|
barMaxVisibleRunningApps: { def: 0 },
|
||||||
@@ -202,6 +203,7 @@ var SPEC = {
|
|||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
rememberLastQuery: { def: false },
|
rememberLastQuery: { def: false },
|
||||||
|
rememberLastMode: { def: true },
|
||||||
spotlightSectionViewModes: { def: {} },
|
spotlightSectionViewModes: { def: {} },
|
||||||
appDrawerSectionViewModes: { def: {} },
|
appDrawerSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
@@ -213,7 +215,9 @@ var SPEC = {
|
|||||||
dankLauncherV2UnloadOnClose: { def: false },
|
dankLauncherV2UnloadOnClose: { def: false },
|
||||||
dankLauncherV2IncludeFilesInAll: { def: false },
|
dankLauncherV2IncludeFilesInAll: { def: false },
|
||||||
dankLauncherV2IncludeFoldersInAll: { def: false },
|
dankLauncherV2IncludeFoldersInAll: { def: false },
|
||||||
|
launcherUseOverlayLayer: { def: false },
|
||||||
launcherStyle: { def: "full" },
|
launcherStyle: { def: "full" },
|
||||||
|
spotlightBarShowModeChips: { def: false },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
@@ -332,7 +336,7 @@ var SPEC = {
|
|||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
dockSmartAutoHide: { def: false },
|
dockSmartAutoHide: { def: false },
|
||||||
dockHideOnFullscreen: { def: true },
|
dockUseOverlayLayer: { def: false },
|
||||||
dockGroupByApp: { def: false },
|
dockGroupByApp: { def: false },
|
||||||
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
dockRestoreSpecialWorkspaceOnClick: { def: false },
|
||||||
dockOpenOnOverview: { def: false },
|
dockOpenOnOverview: { def: false },
|
||||||
@@ -395,6 +399,7 @@ var SPEC = {
|
|||||||
notificationTimeoutNormal: { def: 5000 },
|
notificationTimeoutNormal: { def: 5000 },
|
||||||
notificationTimeoutCritical: { def: 0 },
|
notificationTimeoutCritical: { def: 0 },
|
||||||
notificationCompactMode: { def: false },
|
notificationCompactMode: { def: false },
|
||||||
|
notificationDedupeEnabled: { def: true },
|
||||||
notificationPopupPosition: { def: 0 },
|
notificationPopupPosition: { def: 0 },
|
||||||
notificationAnimationSpeed: { def: 1 },
|
notificationAnimationSpeed: { def: 1 },
|
||||||
notificationCustomAnimationDuration: { def: 400 },
|
notificationCustomAnimationDuration: { def: 400 },
|
||||||
@@ -496,7 +501,7 @@ var SPEC = {
|
|||||||
popupGapsAuto: true,
|
popupGapsAuto: true,
|
||||||
popupGapsManual: 4,
|
popupGapsManual: 4,
|
||||||
maximizeDetection: true,
|
maximizeDetection: true,
|
||||||
fullscreenDetection: true,
|
useOverlayLayer: false,
|
||||||
scrollEnabled: true,
|
scrollEnabled: true,
|
||||||
scrollXBehavior: "column",
|
scrollXBehavior: "column",
|
||||||
scrollYBehavior: "workspace",
|
scrollYBehavior: "workspace",
|
||||||
@@ -573,7 +578,6 @@ var SPEC = {
|
|||||||
frameCloseGaps: { def: true },
|
frameCloseGaps: { def: true },
|
||||||
frameLauncherEmergeSide: { def: "bottom" },
|
frameLauncherEmergeSide: { def: "bottom" },
|
||||||
frameLauncherArcExtender: { def: false },
|
frameLauncherArcExtender: { def: false },
|
||||||
frameUseSpotlightLauncher: { def: false },
|
|
||||||
frameMode: { def: "connected" }
|
frameMode: { def: "connected" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+150
-4
@@ -30,6 +30,7 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("DMSShell")
|
readonly property var log: Log.scoped("DMSShell")
|
||||||
|
readonly property var _sessionsServiceRef: SessionsService
|
||||||
|
|
||||||
property bool osdSurfacesLoaded: true
|
property bool osdSurfacesLoaded: true
|
||||||
property int pendingOsdResumeReloads: 0
|
property int pendingOsdResumeReloads: 0
|
||||||
@@ -63,15 +64,27 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool wallpaperSurfacesLoaded: true
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: blurredWallpaperBackgroundLoader
|
id: blurredWallpaperBackgroundLoader
|
||||||
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
|
|
||||||
sourceComponent: BlurredWallpaperBackground {}
|
sourceComponent: BlurredWallpaperBackground {}
|
||||||
}
|
}
|
||||||
|
|
||||||
WallpaperBackground {}
|
DeferredAction {
|
||||||
|
id: wallpaperSurfaceReloadAction
|
||||||
|
onTriggered: root.wallpaperSurfacesLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: wallpaperBackgroundLoader
|
||||||
|
active: root.wallpaperSurfacesLoaded
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: WallpaperBackground {}
|
||||||
|
}
|
||||||
|
|
||||||
DesktopWidgetLayer {}
|
DesktopWidgetLayer {}
|
||||||
|
|
||||||
@@ -168,6 +181,8 @@ Item {
|
|||||||
property bool barSurfacesLoaded: true
|
property bool barSurfacesLoaded: true
|
||||||
|
|
||||||
function recreateBarSurfaces() {
|
function recreateBarSurfaces() {
|
||||||
|
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","));
|
||||||
if (barSurfacesLoaded)
|
if (barSurfacesLoaded)
|
||||||
barSurfacesLoaded = false;
|
barSurfacesLoaded = false;
|
||||||
barSurfaceReloadAction.schedule();
|
barSurfaceReloadAction.schedule();
|
||||||
@@ -217,7 +232,18 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Frame {}
|
property bool frameSurfacesLoaded: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.frameSurfacesLoaded
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: Frame {}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeferredAction {
|
||||||
|
id: frameSurfaceReloadAction
|
||||||
|
onTriggered: root.frameSurfacesLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dankBarRepeater
|
id: dankBarRepeater
|
||||||
@@ -301,6 +327,81 @@ Item {
|
|||||||
onTriggered: root.osdSurfacesLoaded = true
|
onTriggered: root.osdSurfacesLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool hadRealScreen: true
|
||||||
|
|
||||||
|
function _hasRealScreen() {
|
||||||
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||||
|
if (Quickshell.screens[i].name.length > 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSurfaceRecovery(source) {
|
||||||
|
log.info("Surface recovery triggered by:", source,
|
||||||
|
"screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","),
|
||||||
|
"barLoaded:", root.barSurfacesLoaded,
|
||||||
|
"frameLoaded:", root.frameSurfacesLoaded,
|
||||||
|
"dockEnabled:", root.dockEnabled);
|
||||||
|
surfaceResumeRecoveryTimer.pass = 0;
|
||||||
|
surfaceResumeRecoveryTimer.interval = 800;
|
||||||
|
surfaceResumeRecoveryTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
const hasReal = root._hasRealScreen();
|
||||||
|
log.info("Screens changed:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => "'" + s.name + "'").join(","),
|
||||||
|
"hasReal:", hasReal, "hadReal:", root.hadRealScreen);
|
||||||
|
if (!root.hadRealScreen && hasReal) {
|
||||||
|
log.info("Real screen reappeared after placeholder state, triggering surface recovery");
|
||||||
|
root.triggerSurfaceRecovery("screen-reconnect");
|
||||||
|
}
|
||||||
|
root.hadRealScreen = hasReal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: surfaceResumeRecoveryTimer
|
||||||
|
interval: 800
|
||||||
|
repeat: false
|
||||||
|
property int pass: 0
|
||||||
|
onTriggered: {
|
||||||
|
pass++;
|
||||||
|
log.info("Surface recovery pass", pass,
|
||||||
|
"screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","));
|
||||||
|
|
||||||
|
root.recreateBarSurfaces();
|
||||||
|
|
||||||
|
if (root.frameSurfacesLoaded) {
|
||||||
|
root.frameSurfacesLoaded = false;
|
||||||
|
frameSurfaceReloadAction.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.wallpaperSurfacesLoaded) {
|
||||||
|
root.wallpaperSurfacesLoaded = false;
|
||||||
|
wallpaperSurfaceReloadAction.schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
root.dockEnabled = false;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
root.dockEnabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pass < 2) {
|
||||||
|
interval = 2000;
|
||||||
|
restart();
|
||||||
|
} else {
|
||||||
|
pass = 0;
|
||||||
|
interval = 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
dockRecreateDebounce.start();
|
dockRecreateDebounce.start();
|
||||||
// Force PolkitService singleton to initialize
|
// Force PolkitService singleton to initialize
|
||||||
@@ -725,6 +826,25 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: spotlightBarModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.spotlightBarModalLoader = spotlightBarModalLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
DankLauncherV2ModalSpotlight {
|
||||||
|
id: spotlightBarModal
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
PopoutService.spotlightBarModal = spotlightBarModal;
|
||||||
|
PopoutService._onSpotlightBarModalLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: clipboardHistoryPopoutLoader
|
id: clipboardHistoryPopoutLoader
|
||||||
|
|
||||||
@@ -868,9 +988,17 @@ Item {
|
|||||||
target: SessionService
|
target: SessionService
|
||||||
|
|
||||||
function onSessionResumed() {
|
function onSessionResumed() {
|
||||||
|
log.info("Session resumed: screens:", Quickshell.screens.length,
|
||||||
|
Quickshell.screens.map(s => s.name).join(","),
|
||||||
|
"barLoaded:", root.barSurfacesLoaded,
|
||||||
|
"frameLoaded:", root.frameSurfacesLoaded,
|
||||||
|
"dockEnabled:", root.dockEnabled);
|
||||||
|
|
||||||
root.pendingOsdResumeReloads = 2;
|
root.pendingOsdResumeReloads = 2;
|
||||||
osdResumeRecreateTimer.interval = 400;
|
osdResumeRecreateTimer.interval = 400;
|
||||||
osdResumeRecreateTimer.restart();
|
osdResumeRecreateTimer.restart();
|
||||||
|
|
||||||
|
root.triggerSurfaceRecovery("sessionResumed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1019,12 +1147,30 @@ Item {
|
|||||||
lock.activate();
|
lock.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSwitchUserRequested: {
|
||||||
|
switchUserModalLoader.active = true;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (switchUserModalLoader.item)
|
||||||
|
switchUserModalLoader.item.showFromPowerMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
PopoutService.powerMenuModal = powerMenuModal;
|
PopoutService.powerMenuModal = powerMenuModal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: switchUserModalLoader
|
||||||
|
|
||||||
|
active: false
|
||||||
|
|
||||||
|
SwitchUserModal {
|
||||||
|
id: switchUserModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
id: hyprKeybindsModalLoader
|
id: hyprKeybindsModalLoader
|
||||||
|
|
||||||
@@ -1095,7 +1241,7 @@ Item {
|
|||||||
Variants {
|
Variants {
|
||||||
model: SettingsData.getFilteredScreens("osd")
|
model: SettingsData.getFilteredScreens("osd")
|
||||||
|
|
||||||
delegate: MicMuteOSD {
|
delegate: MicVolumeOSD {
|
||||||
modelData: item
|
modelData: item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1340,6 +1340,25 @@ Item {
|
|||||||
target: "spotlight"
|
target: "spotlight"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
function open(): string {
|
||||||
|
PopoutService.openSpotlightBar();
|
||||||
|
return "SPOTLIGHT_BAR_OPEN_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): string {
|
||||||
|
PopoutService.closeSpotlightBar();
|
||||||
|
return "SPOTLIGHT_BAR_CLOSE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(): string {
|
||||||
|
PopoutService.toggleSpotlightBar();
|
||||||
|
return "SPOTLIGHT_BAR_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
target: "spotlight-bar"
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function info(message: string): string {
|
function info(message: string): string {
|
||||||
if (!message)
|
if (!message)
|
||||||
@@ -1775,6 +1794,36 @@ Item {
|
|||||||
target: "outputs"
|
target: "outputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "mic"
|
||||||
|
|
||||||
|
function setvolume(percentage: string): string {
|
||||||
|
return AudioService.setMicVolume(parseInt(percentage));
|
||||||
|
}
|
||||||
|
|
||||||
|
function increment(step: string): string {
|
||||||
|
return AudioService.incrementMicVolume(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrement(step: string): string {
|
||||||
|
return AudioService.decrementMicVolume(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mute(): string {
|
||||||
|
return AudioService.toggleMicMute();
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) {
|
||||||
|
return "No audio source available";
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = Math.round(AudioService.source.audio.volume * 100);
|
||||||
|
const muteStatus = AudioService.source.audio.muted ? " (muted)" : "";
|
||||||
|
return `Microphone: ${volume}%${muteStatus}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function findTrayItem(itemId: string): var {
|
function findTrayItem(itemId: string): var {
|
||||||
if (!itemId)
|
if (!itemId)
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ Item {
|
|||||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||||
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +205,7 @@ Item {
|
|||||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||||
|
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,519 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
property var keyController: null
|
||||||
|
|
||||||
|
property var entry: null
|
||||||
|
property string editorText: ""
|
||||||
|
|
||||||
|
function decodeEntryData(data) {
|
||||||
|
if (!data) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof data !== "string") {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = data.replace(/\s+/g, "");
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chars = new Array(sanitized.length);
|
||||||
|
for (let i = 0; i < sanitized.length; i++) {
|
||||||
|
chars[i] = sanitized.charAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
buffer = bytes.buffer;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEntry(newEntry) {
|
||||||
|
entry = newEntry;
|
||||||
|
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
||||||
|
if (editField) {
|
||||||
|
editField.text = editorText;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (editField) {
|
||||||
|
editField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newEntry || newEntry.isImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedId = newEntry.id;
|
||||||
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
|
"id": requestedId
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!root.entry || root.entry.id !== requestedId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = response.result;
|
||||||
|
let fullText = "";
|
||||||
|
if (result?.data) {
|
||||||
|
fullText = root.decodeEntryData(result.data);
|
||||||
|
} else {
|
||||||
|
fullText = result?.preview ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullText || fullText.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.editorText = fullText;
|
||||||
|
if (editField) {
|
||||||
|
editField.text = fullText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEntry(action) {
|
||||||
|
const saveAction = action ?? "history";
|
||||||
|
DMSService.sendRequest("clipboard.copy", {
|
||||||
|
"text": root.editorText
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "history") {
|
||||||
|
modal.mode = "history";
|
||||||
|
Qt.callLater(function () {
|
||||||
|
ClipboardService.reset();
|
||||||
|
ClipboardService.refresh();
|
||||||
|
if (keyController) {
|
||||||
|
keyController.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "close") {
|
||||||
|
modal.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "paste") {
|
||||||
|
ClipboardService.pasteClipboard(modal.hide);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionSaveMenu() {
|
||||||
|
saveMenu.width = Math.max(saveMenuColumn.implicitWidth + saveMenu.padding * 2, saveButton.width);
|
||||||
|
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
|
||||||
|
const popupW = saveMenu.width;
|
||||||
|
const popupH = saveMenu.height;
|
||||||
|
const overlayW = Overlay.overlay.width;
|
||||||
|
const overlayH = Overlay.overlay.height;
|
||||||
|
|
||||||
|
let x = pos.x + (saveButton.width - popupW) / 2;
|
||||||
|
let y = pos.y + saveButton.height + 4;
|
||||||
|
if (y + popupH > overlayH) {
|
||||||
|
y = pos.y - popupH - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
|
||||||
|
y = Math.max(8, y);
|
||||||
|
|
||||||
|
saveMenu.x = x;
|
||||||
|
saveMenu.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSaveMenu() {
|
||||||
|
if (saveMenu.visible) {
|
||||||
|
saveMenu.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveMenu.open();
|
||||||
|
positionSaveMenu();
|
||||||
|
Qt.callLater(positionSaveMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequences: ["Escape"]
|
||||||
|
enabled: modal.mode === "editor"
|
||||||
|
onActivated: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: editorHeader
|
||||||
|
width: parent.width
|
||||||
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "arrow_back"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit Clipboard")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: editFieldContainer
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium
|
||||||
|
border.width: editField.activeFocus ? 2 : 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: editIcon
|
||||||
|
name: "edit"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: editScroll
|
||||||
|
anchors.left: editIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: editField.height
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: editField
|
||||||
|
width: editScroll.width
|
||||||
|
height: Math.max(editScroll.height, contentHeight)
|
||||||
|
text: root.editorText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
selectByMouse: true
|
||||||
|
onTextChanged: root.editorText = text
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
||||||
|
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
|
||||||
|
|
||||||
|
if (hasCtrl && event.key === Qt.Key_S) {
|
||||||
|
root.saveEntry(hasShift ? "close" : "history");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
|
||||||
|
root.saveEntry("paste");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit clipboard text")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.outlineButton
|
||||||
|
anchors.left: editScroll.left
|
||||||
|
anchors.right: editScroll.right
|
||||||
|
anchors.top: editScroll.top
|
||||||
|
anchors.bottom: editScroll.bottom
|
||||||
|
visible: editField.text.length === 0 && !editField.activeFocus
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: editorActions
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: buttonSpacer
|
||||||
|
width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS)
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: cancelButton
|
||||||
|
text: I18n.tr("Cancel")
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveButton
|
||||||
|
|
||||||
|
readonly property int buttonHeight: cancelButton.buttonHeight
|
||||||
|
readonly property int arrowWidth: Theme.iconSizeLarge
|
||||||
|
|
||||||
|
width: cancelButton.width
|
||||||
|
height: buttonHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveMainArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveMainArea
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveArrowArea
|
||||||
|
width: saveButton.arrowWidth
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
height: parent.height - cancelButton.horizontalPadding
|
||||||
|
color: Theme.withAlpha(Theme.onPrimary, 0.2)
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: saveMenu.visible ? "expand_less" : "expand_more"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveArrowArea
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
z: 1
|
||||||
|
anchors.fill: saveMainArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.saveEntry("history")
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
z: 1
|
||||||
|
anchors.fill: saveArrowArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.toggleSaveMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: saveMenu
|
||||||
|
parent: Overlay.overlay
|
||||||
|
padding: Theme.spacingM
|
||||||
|
modal: true
|
||||||
|
dim: false
|
||||||
|
focus: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Column {
|
||||||
|
id: saveMenuColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "save"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuSaveArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("history");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuCloseRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "close"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and close")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuCloseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: modal.wtypeAvailable ? 1 : 0.5
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuPasteRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "content_paste"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and paste")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuPasteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: modal.wtypeAvailable
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("paste");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ Rectangle {
|
|||||||
signal deleteRequested
|
signal deleteRequested
|
||||||
signal pinRequested
|
signal pinRequested
|
||||||
signal unpinRequested
|
signal unpinRequested
|
||||||
|
signal editRequested
|
||||||
|
|
||||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||||
@@ -70,6 +71,20 @@ Rectangle {
|
|||||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "edit"
|
||||||
|
iconSize: Theme.iconSize - 6
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (entryType === "image") {
|
||||||
|
// TODO - forward to editing software
|
||||||
|
} else {
|
||||||
|
editRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
iconName: "close"
|
iconName: "close"
|
||||||
iconSize: Theme.iconSize - 6
|
iconSize: Theme.iconSize - 6
|
||||||
@@ -142,8 +157,11 @@ Rectangle {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
anchors.fill: parent
|
anchors.left: parent.left
|
||||||
anchors.rightMargin: 80
|
anchors.right: actionButtons.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => {
|
onPressed: mouse => {
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ DankModal {
|
|||||||
service: ClipboardService
|
service: ClipboardService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property string mode: "history"
|
||||||
|
onModeChanged: {
|
||||||
|
if (mode !== "history") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (contentLoader.item?.searchField) {
|
||||||
|
contentLoader.item.searchField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateFilteredModel() {
|
function updateFilteredModel() {
|
||||||
ClipboardService.updateFilteredModel();
|
ClipboardService.updateFilteredModel();
|
||||||
}
|
}
|
||||||
@@ -61,6 +73,7 @@ DankModal {
|
|||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
open();
|
open();
|
||||||
|
mode = "history";
|
||||||
activeImageLoads = 0;
|
activeImageLoads = 0;
|
||||||
shouldHaveFocus = true;
|
shouldHaveFocus = true;
|
||||||
ClipboardService.reset();
|
ClipboardService.reset();
|
||||||
@@ -130,6 +143,21 @@ DankModal {
|
|||||||
return ClipboardService.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
|
visible: false
|
||||||
modalWidth: ClipboardConstants.modalWidth
|
modalWidth: ClipboardConstants.modalWidth
|
||||||
modalHeight: ClipboardConstants.modalHeight
|
modalHeight: ClipboardConstants.modalHeight
|
||||||
@@ -138,6 +166,7 @@ DankModal {
|
|||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
enableShadow: true
|
enableShadow: true
|
||||||
|
closeOnEscapeKey: mode !== "editor"
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
keyboardController.handleKey(event);
|
keyboardController.handleKey(event);
|
||||||
@@ -174,9 +203,109 @@ DankModal {
|
|||||||
property var confirmDialog: clearConfirmDialog
|
property var confirmDialog: clearConfirmDialog
|
||||||
|
|
||||||
clipboardContent: Component {
|
clipboardContent: Component {
|
||||||
ClipboardContent {
|
Item {
|
||||||
modal: clipboardHistoryModal
|
id: viewContainer
|
||||||
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,24 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editSelected() {
|
||||||
|
const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
|
||||||
|
modal.editEntry(entries[index]);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKey(event) {
|
function handleKey(event) {
|
||||||
|
if (modal.mode === "editor") {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
modal.mode = "history";
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_Escape:
|
case Qt.Key_Escape:
|
||||||
if (ClipboardService.keyboardNavigationActive) {
|
if (ClipboardService.keyboardNavigationActive) {
|
||||||
@@ -152,6 +169,10 @@ QtObject {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_E:
|
||||||
|
editSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Rectangle {
|
|||||||
readonly property string hintsText: {
|
readonly property string hintsText: {
|
||||||
if (!wtypeAvailable)
|
if (!wtypeAvailable)
|
||||||
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
|
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
|
||||||
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
|
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • F10: Help • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • F10: Help • Esc: Close");
|
||||||
}
|
}
|
||||||
|
|
||||||
height: ClipboardConstants.keyboardHintsHeight
|
height: ClipboardConstants.keyboardHintsHeight
|
||||||
@@ -22,13 +22,17 @@ Rectangle {
|
|||||||
z: 100
|
z: 100
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
width: parent.width - Theme.spacingL * 2
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
|
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,9 @@ Rectangle {
|
|||||||
text: keyboardHints.hintsText
|
text: keyboardHints.hintsText
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Item {
|
|||||||
|
|
||||||
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
||||||
|
|
||||||
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && !allowStacking
|
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && !allowStacking && CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
|
||||||
|
|
||||||
function _dockOccupiesSide(side) {
|
function _dockOccupiesSide(side) {
|
||||||
if (!SettingsData.showDock)
|
if (!SettingsData.showDock)
|
||||||
@@ -58,7 +58,7 @@ Item {
|
|||||||
|
|
||||||
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
|
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
|
||||||
|
|
||||||
readonly property bool connectedMotionParity: Theme.isConnectedEffect
|
readonly property bool connectedMotionParity: frameOwnsConnectedChrome
|
||||||
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||||
property real animationOffset: Theme.effectAnimOffset
|
property real animationOffset: Theme.effectAnimOffset
|
||||||
@@ -68,7 +68,7 @@ Item {
|
|||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
readonly property bool connectedSurfaceOverride: frameOwnsConnectedChrome
|
||||||
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
|
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
|
||||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
||||||
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||||
@@ -346,7 +346,7 @@ Item {
|
|||||||
readonly property real shadowFallbackOffset: 6
|
readonly property real shadowFallbackOffset: 6
|
||||||
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
readonly property real shadowMotionPadding: {
|
readonly property real shadowMotionPadding: {
|
||||||
if (Theme.isConnectedEffect)
|
if (frameOwnsConnectedChrome)
|
||||||
return 0;
|
return 0;
|
||||||
if (animationType === "slide")
|
if (animationType === "slide")
|
||||||
return 30;
|
return 30;
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ Rectangle {
|
|||||||
|
|
||||||
property var entry: null
|
property var entry: null
|
||||||
property string cachedImageData: ""
|
property string cachedImageData: ""
|
||||||
|
property string cachedMimeType: ""
|
||||||
property var _requestedEntryId: null
|
property var _requestedEntryId: null
|
||||||
|
|
||||||
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
||||||
readonly property string sourceUrl: cachedImageData.length > 0 ? "data:" + (entry?.mimeType ?? "image/png") + ";base64," + cachedImageData : ""
|
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
|
||||||
|
|
||||||
radius: Math.max(6, Theme.cornerRadius - 2)
|
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||||
clip: true
|
clip: true
|
||||||
@@ -24,8 +25,24 @@ Rectangle {
|
|||||||
onEntryChanged: reloadPreview()
|
onEntryChanged: reloadPreview()
|
||||||
Component.onCompleted: reloadPreview()
|
Component.onCompleted: reloadPreview()
|
||||||
|
|
||||||
|
function isImageMimeType(mimeType) {
|
||||||
|
return (mimeType || "").toString().toLowerCase().startsWith("image/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvedSourceUrl(data, mimeType) {
|
||||||
|
const rawData = (data || "").toString();
|
||||||
|
if (rawData.length === 0)
|
||||||
|
return "";
|
||||||
|
if (rawData.startsWith("data:"))
|
||||||
|
return rawData.startsWith("data:image/") ? rawData : "";
|
||||||
|
if (!isImageMimeType(mimeType))
|
||||||
|
return "";
|
||||||
|
return "data:" + mimeType + ";base64," + rawData;
|
||||||
|
}
|
||||||
|
|
||||||
function reloadPreview() {
|
function reloadPreview() {
|
||||||
cachedImageData = "";
|
cachedImageData = "";
|
||||||
|
cachedMimeType = "";
|
||||||
if (!canLoadImage || !entry?.id) {
|
if (!canLoadImage || !entry?.id) {
|
||||||
_requestedEntryId = null;
|
_requestedEntryId = null;
|
||||||
return;
|
return;
|
||||||
@@ -40,9 +57,13 @@ Rectangle {
|
|||||||
return;
|
return;
|
||||||
if (response.error)
|
if (response.error)
|
||||||
return;
|
return;
|
||||||
const data = response.result?.data ?? "";
|
const result = response.result ?? {};
|
||||||
if (data.length > 0)
|
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
|
||||||
cachedImageData = data;
|
const data = (result.data ?? "").toString();
|
||||||
|
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
|
||||||
|
return;
|
||||||
|
cachedMimeType = mimeType;
|
||||||
|
cachedImageData = data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,25 +35,28 @@ Item {
|
|||||||
property int gridColumns: SettingsData.appLauncherGridColumns
|
property int gridColumns: SettingsData.appLauncherGridColumns
|
||||||
property int viewModeVersion: 0
|
property int viewModeVersion: 0
|
||||||
property string viewModeContext: "spotlight"
|
property string viewModeContext: "spotlight"
|
||||||
|
property bool forceLinearNavigation: false
|
||||||
|
|
||||||
signal itemExecuted
|
signal itemExecuted
|
||||||
signal searchCompleted
|
signal searchCompleted
|
||||||
signal modeChanged(string mode)
|
signal modeChanged(string mode, bool userInitiated)
|
||||||
signal queryChanged(string query)
|
signal queryChanged(string query)
|
||||||
signal viewModeChanged(string sectionId, string mode)
|
signal viewModeChanged(string sectionId, string mode)
|
||||||
signal searchQueryRequested(string query)
|
signal searchQueryRequested(string query)
|
||||||
|
|
||||||
|
Ref {
|
||||||
|
service: AppSearchService
|
||||||
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active) {
|
if (!active) {
|
||||||
if (clipboardSearchEnabledInAll())
|
|
||||||
ClipboardService.ensureLauncherHistory();
|
|
||||||
} else {
|
|
||||||
SessionData.addLauncherHistory(searchQuery);
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
|
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
selectedItem = null;
|
selectedItem = null;
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
|
ClipboardService.invalidateLauncherSearchCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +91,25 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ClipboardService
|
target: ClipboardService
|
||||||
function onInternalEntriesChanged() {
|
function onLauncherSearchReady(query) {
|
||||||
if (!active || !clipboardSearchEnabledInAll())
|
if (!active)
|
||||||
return;
|
return;
|
||||||
if (searchMode === "all" && searchQuery.length >= 2)
|
|
||||||
performSearch();
|
const clipboardBuiltInActive = activePluginId === "dms_clipboard_search";
|
||||||
|
if (!clipboardBuiltInActive && !clipboardSearchEnabledInAll())
|
||||||
|
return;
|
||||||
|
if (!clipboardBuiltInActive && searchMode !== "all")
|
||||||
|
return;
|
||||||
|
|
||||||
|
const trimmed = (searchQuery || "").trim();
|
||||||
|
if (trimmed.length < 2 && query.length > 0)
|
||||||
|
return;
|
||||||
|
const triggerMatch = detectTrigger(trimmed);
|
||||||
|
const effectiveQuery = clipboardBuiltInActive && triggerMatch.pluginId === "dms_clipboard_search" ? triggerMatch.query : trimmed;
|
||||||
|
if (query !== effectiveQuery)
|
||||||
|
return;
|
||||||
|
|
||||||
|
searchDebounce.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,8 +420,19 @@ Item {
|
|||||||
searchQuery = query;
|
searchQuery = query;
|
||||||
searchDebounce.restart();
|
searchDebounce.restart();
|
||||||
|
|
||||||
if (searchMode === "all" && clipboardSearchEnabledInAll() && query.length >= 2)
|
if (searchMode !== "plugins" && query.startsWith("/")) {
|
||||||
ClipboardService.ensureLauncherHistory();
|
var prefix = Utils.parseFileSearchPrefix(query);
|
||||||
|
var explicitType = prefix && prefix.type !== null ? prefix.type : null;
|
||||||
|
var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all");
|
||||||
|
if (searchMode !== "files") {
|
||||||
|
setMode("files", true, targetType);
|
||||||
|
} else if (fileSearchType !== targetType) {
|
||||||
|
fileSearchType = targetType;
|
||||||
|
}
|
||||||
|
if (explicitType !== null && SessionData.launcherLastFileSearchType !== explicitType) {
|
||||||
|
SessionData.setLauncherLastFileSearchType(explicitType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
|
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
|
||||||
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
|
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
|
||||||
@@ -412,9 +440,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMode(mode, isAutoSwitch) {
|
function setMode(mode, isAutoSwitch, fileTypeOverride, notPersist) {
|
||||||
if (searchMode === mode)
|
if (searchMode === mode) {
|
||||||
|
if (mode === "files" && fileTypeOverride !== undefined && fileSearchType !== fileTypeOverride) {
|
||||||
|
fileSearchType = fileTypeOverride;
|
||||||
|
performFileSearch();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
if (isAutoSwitch) {
|
if (isAutoSwitch) {
|
||||||
previousSearchMode = searchMode;
|
previousSearchMode = searchMode;
|
||||||
autoSwitchedToFiles = true;
|
autoSwitchedToFiles = true;
|
||||||
@@ -422,10 +455,11 @@ Item {
|
|||||||
autoSwitchedToFiles = false;
|
autoSwitchedToFiles = false;
|
||||||
}
|
}
|
||||||
searchMode = mode;
|
searchMode = mode;
|
||||||
modeChanged(mode);
|
if (mode === "files") {
|
||||||
|
fileSearchType = fileTypeOverride !== undefined ? fileTypeOverride : (SessionData.launcherLastFileSearchType || "all");
|
||||||
|
}
|
||||||
|
modeChanged(mode, !isAutoSwitch && notPersist !== true);
|
||||||
performSearch();
|
performSearch();
|
||||||
if (mode === "all" && clipboardSearchEnabledInAll() && searchQuery.length >= 2)
|
|
||||||
ClipboardService.ensureLauncherHistory();
|
|
||||||
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
||||||
if (mode === "files" || filesInAll) {
|
if (mode === "files" || filesInAll) {
|
||||||
fileSearchDebounce.restart();
|
fileSearchDebounce.restart();
|
||||||
@@ -437,7 +471,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
autoSwitchedToFiles = false;
|
autoSwitchedToFiles = false;
|
||||||
searchMode = previousSearchMode;
|
searchMode = previousSearchMode;
|
||||||
modeChanged(previousSearchMode);
|
modeChanged(previousSearchMode, false);
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +567,7 @@ Item {
|
|||||||
if (fileSearchType === type)
|
if (fileSearchType === type)
|
||||||
return;
|
return;
|
||||||
fileSearchType = type;
|
fileSearchType = type;
|
||||||
|
SessionData.setLauncherLastFileSearchType(type);
|
||||||
performFileSearch();
|
performFileSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +738,8 @@ Item {
|
|||||||
clearActivePluginViewPreference();
|
clearActivePluginViewPreference();
|
||||||
|
|
||||||
if (searchMode === "files") {
|
if (searchMode === "files") {
|
||||||
var fileQuery = searchQuery.startsWith("/") ? searchQuery.substring(1).trim() : searchQuery.trim();
|
var prefixInfo = Utils.parseFileSearchPrefix(searchQuery);
|
||||||
|
var fileQuery = prefixInfo ? prefixInfo.query : searchQuery.trim();
|
||||||
isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable;
|
isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable;
|
||||||
sections = [];
|
sections = [];
|
||||||
flatModel = [];
|
flatModel = [];
|
||||||
@@ -993,7 +1029,8 @@ Item {
|
|||||||
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
|
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
|
||||||
|
|
||||||
if (searchQuery.startsWith("/")) {
|
if (searchQuery.startsWith("/")) {
|
||||||
fileQuery = searchQuery.substring(1).trim();
|
var prefixInfo = Utils.parseFileSearchPrefix(searchQuery);
|
||||||
|
fileQuery = prefixInfo ? prefixInfo.query : searchQuery.substring(1).trim();
|
||||||
} else if (searchMode === "files") {
|
} else if (searchMode === "files") {
|
||||||
fileQuery = searchQuery.trim();
|
fileQuery = searchQuery.trim();
|
||||||
} else if (searchMode === "all" && (includeFiles || includeFolders)) {
|
} else if (searchMode === "all" && (includeFiles || includeFolders)) {
|
||||||
@@ -1209,7 +1246,6 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (clipboardSearchEnabledInAll()) {
|
if (clipboardSearchEnabledInAll()) {
|
||||||
ClipboardService.ensureLauncherHistory();
|
|
||||||
var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query);
|
var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query);
|
||||||
var clipboardLimit = Math.min(clipboardItems.length, 8);
|
var clipboardLimit = Math.min(clipboardItems.length, 8);
|
||||||
for (var j = 0; j < clipboardLimit; j++) {
|
for (var j = 0; j < clipboardLimit; j++) {
|
||||||
@@ -1713,7 +1749,9 @@ Item {
|
|||||||
function selectNext() {
|
function selectNext() {
|
||||||
keyboardNavigationActive = true;
|
keyboardNavigationActive = true;
|
||||||
_cancelPendingSelectionReset();
|
_cancelPendingSelectionReset();
|
||||||
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
|
var newIndex = forceLinearNavigation ? Nav.findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1) : Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
|
||||||
|
if (newIndex === -1)
|
||||||
|
newIndex = selectedFlatIndex;
|
||||||
if (newIndex !== selectedFlatIndex) {
|
if (newIndex !== selectedFlatIndex) {
|
||||||
selectedFlatIndex = newIndex;
|
selectedFlatIndex = newIndex;
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
@@ -1723,7 +1761,9 @@ Item {
|
|||||||
function selectPrevious() {
|
function selectPrevious() {
|
||||||
keyboardNavigationActive = true;
|
keyboardNavigationActive = true;
|
||||||
_cancelPendingSelectionReset();
|
_cancelPendingSelectionReset();
|
||||||
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
|
var newIndex = forceLinearNavigation ? Nav.findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1) : Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
|
||||||
|
if (newIndex === -1)
|
||||||
|
newIndex = selectedFlatIndex;
|
||||||
if (newIndex !== selectedFlatIndex) {
|
if (newIndex !== selectedFlatIndex) {
|
||||||
selectedFlatIndex = newIndex;
|
selectedFlatIndex = newIndex;
|
||||||
updateSelectedItem();
|
updateSelectedItem();
|
||||||
@@ -1857,7 +1897,7 @@ Item {
|
|||||||
if (browseTrigger && browseTrigger.length > 0) {
|
if (browseTrigger && browseTrigger.length > 0) {
|
||||||
searchQueryRequested(browseTrigger);
|
searchQueryRequested(browseTrigger);
|
||||||
} else {
|
} else {
|
||||||
setMode("plugins");
|
setMode("plugins", false, undefined, true);
|
||||||
pluginFilter = browsePluginId;
|
pluginFilter = browsePluginId;
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,3 +159,14 @@ function sortPluginsOrdered(plugins, order) {
|
|||||||
return aOrder - bOrder;
|
return aOrder - bOrder;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseFileSearchPrefix(query) {
|
||||||
|
if (!query || !query.startsWith("/"))
|
||||||
|
return null;
|
||||||
|
var rest = query.substring(1);
|
||||||
|
if (rest === "d" || rest.startsWith("d ") || rest.startsWith("d\t"))
|
||||||
|
return { type: "dir", query: rest.substring(1).trim() };
|
||||||
|
if (rest === "f" || rest.startsWith("f ") || rest.startsWith("f\t"))
|
||||||
|
return { type: "file", query: rest.substring(1).trim() };
|
||||||
|
return { type: null, query: rest.trim() };
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Item {
|
|||||||
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
|
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
|
||||||
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
|
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
|
||||||
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
|
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
|
||||||
|
property bool triggerUsesOverlayLayer: false
|
||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ Item {
|
|||||||
impl.item.toggleWithMode(mode);
|
impl.item.toggleWithMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool useSpotlightBackend: SettingsData.connectedFrameModeActive ? SettingsData.frameUseSpotlightLauncher : SettingsData.launcherStyle === "spotlight"
|
readonly property bool useSpotlightBackend: !SettingsData.connectedFrameModeActive && SettingsData.launcherStyle === "spotlight"
|
||||||
readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp)
|
readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp)
|
||||||
property var _resolvedBackend: null
|
property var _resolvedBackend: null
|
||||||
|
|
||||||
@@ -72,9 +73,6 @@ Item {
|
|||||||
function onConnectedFrameModeActiveChanged() {
|
function onConnectedFrameModeActiveChanged() {
|
||||||
root._maybeResolveBackend();
|
root._maybeResolveBackend();
|
||||||
}
|
}
|
||||||
function onFrameUseSpotlightLauncherChanged() {
|
|
||||||
root._maybeResolveBackend();
|
|
||||||
}
|
|
||||||
function onLauncherStyleChanged() {
|
function onLauncherStyleChanged() {
|
||||||
root._maybeResolveBackend();
|
root._maybeResolveBackend();
|
||||||
}
|
}
|
||||||
@@ -116,6 +114,7 @@ Item {
|
|||||||
if (!it)
|
if (!it)
|
||||||
return;
|
return;
|
||||||
it.modalHandle = root;
|
it.modalHandle = root;
|
||||||
|
it.triggerUsesOverlayLayer = Qt.binding(() => root.triggerUsesOverlayLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ Item {
|
|||||||
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
|
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
|
||||||
|
|
||||||
property var modalHandle: root
|
property var modalHandle: root
|
||||||
|
property bool triggerUsesOverlayLayer: false
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
property bool spotlightOpen: false
|
property bool spotlightOpen: false
|
||||||
property bool keyboardActive: false
|
property bool keyboardActive: false
|
||||||
property bool contentVisible: false
|
property bool contentVisible: false
|
||||||
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
|
readonly property bool launcherMotionVisible: frameOwnsConnectedChrome ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
|
||||||
property var spotlightContent: launcherContentLoader.item
|
property var spotlightContent: launcherContentLoader.item
|
||||||
property bool openedFromOverview: false
|
property bool openedFromOverview: false
|
||||||
property bool isClosing: false
|
property bool isClosing: false
|
||||||
@@ -40,6 +41,21 @@ Item {
|
|||||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
|
readonly property bool usesOverlayLayer: SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
|
||||||
|
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: {
|
readonly property int baseWidth: {
|
||||||
switch (SettingsData.dankLauncherV2Size) {
|
switch (SettingsData.dankLauncherV2Size) {
|
||||||
@@ -74,7 +90,7 @@ Item {
|
|||||||
|
|
||||||
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
|
||||||
|
|
||||||
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== ""
|
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
|
||||||
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
|
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
|
||||||
|
|
||||||
function _dockOccupiesSide(side) {
|
function _dockOccupiesSide(side) {
|
||||||
@@ -140,10 +156,10 @@ Item {
|
|||||||
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
|
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
|
||||||
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
|
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
|
||||||
|
|
||||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
readonly property bool connectedSurfaceOverride: frameOwnsConnectedChrome
|
||||||
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
readonly property int launcherAnimationDuration: frameOwnsConnectedChrome ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||||
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
readonly property list<real> launcherEnterCurve: frameOwnsConnectedChrome ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
||||||
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
readonly property list<real> launcherExitCurve: frameOwnsConnectedChrome ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
||||||
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||||
readonly property color borderColor: {
|
readonly property color borderColor: {
|
||||||
@@ -372,6 +388,7 @@ Item {
|
|||||||
if (!spotlightContent)
|
if (!spotlightContent)
|
||||||
return;
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
|
spotlightContent.closeTransientUi?.();
|
||||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||||
|
|
||||||
@@ -379,12 +396,12 @@ Item {
|
|||||||
spotlightContent.searchField.text = query;
|
spotlightContent.searchField.text = query;
|
||||||
}
|
}
|
||||||
if (spotlightContent.controller) {
|
if (spotlightContent.controller) {
|
||||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
var targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
spotlightContent.controller.searchMode = targetMode;
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
spotlightContent.controller.activePluginId = "";
|
spotlightContent.controller.activePluginId = "";
|
||||||
spotlightContent.controller.activePluginName = "";
|
spotlightContent.controller.activePluginName = "";
|
||||||
spotlightContent.controller.pluginFilter = "";
|
spotlightContent.controller.pluginFilter = "";
|
||||||
spotlightContent.controller.fileSearchType = "all";
|
spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
|
||||||
spotlightContent.controller.fileSearchExt = "";
|
spotlightContent.controller.fileSearchExt = "";
|
||||||
spotlightContent.controller.fileSearchFolder = "";
|
spotlightContent.controller.fileSearchFolder = "";
|
||||||
spotlightContent.controller.fileSearchSort = "score";
|
spotlightContent.controller.fileSearchSort = "score";
|
||||||
@@ -464,6 +481,7 @@ Item {
|
|||||||
function hide() {
|
function hide() {
|
||||||
if (!spotlightOpen)
|
if (!spotlightOpen)
|
||||||
return;
|
return;
|
||||||
|
spotlightContent?.closeTransientUi?.();
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||||
@@ -521,8 +539,8 @@ Item {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode)
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
@@ -596,7 +614,7 @@ Item {
|
|||||||
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
@@ -669,20 +687,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: root.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 WlrLayershell.Top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
@@ -923,8 +928,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Item {
|
|||||||
readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight")
|
readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight")
|
||||||
|
|
||||||
property var modalHandle: root
|
property var modalHandle: root
|
||||||
|
property bool triggerUsesOverlayLayer: false
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -29,13 +30,29 @@ Item {
|
|||||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
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: {
|
||||||
|
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: 80
|
readonly property int _openDuration: 50
|
||||||
readonly property int _closeDuration: 70
|
readonly property int _closeDuration: 40
|
||||||
readonly property int _motionDuration: 90
|
readonly property int _motionDuration: 60
|
||||||
|
|
||||||
// Connected frame mode clamps the centered surface inside frame insets.
|
// Connected frame mode clamps the centered surface inside frame insets.
|
||||||
readonly property bool frameConnected: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
readonly property bool frameConnected: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
|
||||||
|
|
||||||
function _frameEdgeInset(side) {
|
function _frameEdgeInset(side) {
|
||||||
if (!effectiveScreen || !frameConnected)
|
if (!effectiveScreen || !frameConnected)
|
||||||
@@ -58,7 +75,7 @@ Item {
|
|||||||
const searchBarH = 56;
|
const searchBarH = 56;
|
||||||
const usableH = Math.max(searchBarH, screenHeight - insetT - insetB);
|
const usableH = Math.max(searchBarH, screenHeight - insetT - insetB);
|
||||||
const preferred = insetT + Math.max(0, usableH * 0.33 - searchBarH / 2);
|
const preferred = insetT + Math.max(0, usableH * 0.33 - searchBarH / 2);
|
||||||
const maxY = Math.max(insetT, screenHeight - insetB - _contentImplicitH);
|
const maxY = Math.max(insetT, screenHeight - insetB - 56);
|
||||||
return Math.max(insetT, Math.min(preferred, maxY));
|
return Math.max(insetT, Math.min(preferred, maxY));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,9 +142,10 @@ Item {
|
|||||||
if (!spotlightContent)
|
if (!spotlightContent)
|
||||||
return;
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
|
spotlightContent.closeTransientUi?.();
|
||||||
|
|
||||||
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
|
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
|
||||||
const targetMode = mode || SessionData.launcherLastMode || "all";
|
const targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
|
|
||||||
if (spotlightContent.searchField) {
|
if (spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.text = targetQuery;
|
spotlightContent.searchField.text = targetQuery;
|
||||||
@@ -185,6 +203,7 @@ Item {
|
|||||||
function hide() {
|
function hide() {
|
||||||
if (!spotlightOpen)
|
if (!spotlightOpen)
|
||||||
return;
|
return;
|
||||||
|
spotlightContent?.closeTransientUi?.();
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
contentVisible = false;
|
contentVisible = false;
|
||||||
@@ -259,11 +278,11 @@ Item {
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: clickCatcher
|
id: clickCatcher
|
||||||
screen: launcherWindow.screen
|
screen: launcherWindow.screen
|
||||||
visible: spotlightOpen || isClosing
|
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
@@ -324,31 +343,26 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
|
||||||
case "overlay":
|
|
||||||
return WlrLayershell.Overlay;
|
|
||||||
default:
|
|
||||||
return WlrLayershell.Top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
left: true
|
left: true
|
||||||
|
right: root.useBackgroundDarken
|
||||||
|
bottom: root.useBackgroundDarken
|
||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.margins {
|
WlrLayershell.margins {
|
||||||
left: root.windowX
|
left: root.useBackgroundDarken ? 0 : root.windowX
|
||||||
top: root.windowY
|
top: root.useBackgroundDarken ? 0 : root.windowY
|
||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: root.windowWidth
|
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
|
||||||
implicitHeight: root.windowHeight
|
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
|
||||||
|
|
||||||
mask: Region {
|
mask: Region {
|
||||||
item: inputMask
|
item: inputMask
|
||||||
@@ -358,19 +372,44 @@ Item {
|
|||||||
id: inputMask
|
id: inputMask
|
||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
x: modalContainer.x
|
x: root.useBackgroundDarken ? 0 : modalContainer.x
|
||||||
y: modalContainer.y + modalContainer.slideOffset
|
y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset
|
||||||
width: root.alignedWidth
|
width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth
|
||||||
height: root._contentImplicitH
|
height: root.useBackgroundDarken ? launcherWindow.height : root._contentImplicitH
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.useBackgroundDarken && spotlightOpen
|
||||||
|
z: -2
|
||||||
|
onClicked: root.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: backgroundDarken
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
|
||||||
|
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
|
||||||
|
z: -3
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? [0.0, 0.0, 0.2, 1.0, 1.0, 1.0] : [0.4, 0.0, 1.0, 1.0, 1.0, 1.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: modalContainer
|
id: modalContainer
|
||||||
x: root.contentX
|
x: root.useBackgroundDarken ? root.alignedX : root.contentX
|
||||||
y: root.contentY
|
y: root.useBackgroundDarken ? root.alignedY : root.contentY
|
||||||
width: root.alignedWidth
|
width: root.alignedWidth
|
||||||
height: root._animatedContentH
|
height: root._animatedContentH
|
||||||
visible: _renderActive
|
visible: _renderActive
|
||||||
|
z: 0
|
||||||
|
|
||||||
property bool _renderActive: contentVisible
|
property bool _renderActive: contentVisible
|
||||||
property real slideOffset: contentVisible ? 0 : -root._animHeadroom
|
property real slideOffset: contentVisible ? 0 : -root._animHeadroom
|
||||||
@@ -450,8 +489,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Item {
|
|||||||
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
|
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
|
||||||
|
|
||||||
property var modalHandle: root
|
property var modalHandle: root
|
||||||
|
property bool triggerUsesOverlayLayer: false
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ Item {
|
|||||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
|
|
||||||
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
readonly property bool frameOwnsConnectedChrome: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
|
||||||
readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : ""
|
readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : ""
|
||||||
|
|
||||||
readonly property int baseWidth: {
|
readonly property int baseWidth: {
|
||||||
@@ -79,6 +80,21 @@ Item {
|
|||||||
|
|
||||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
|
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
|
||||||
|
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
|
||||||
|
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 real cornerRadius: Theme.cornerRadius
|
||||||
readonly property color borderColor: {
|
readonly property color borderColor: {
|
||||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||||
@@ -117,6 +133,7 @@ Item {
|
|||||||
if (!spotlightContent)
|
if (!spotlightContent)
|
||||||
return;
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
|
spotlightContent.closeTransientUi?.();
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
|
|
||||||
var targetQuery = "";
|
var targetQuery = "";
|
||||||
@@ -131,12 +148,12 @@ Item {
|
|||||||
spotlightContent.searchField.text = targetQuery;
|
spotlightContent.searchField.text = targetQuery;
|
||||||
}
|
}
|
||||||
if (spotlightContent.controller) {
|
if (spotlightContent.controller) {
|
||||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
var targetMode = mode || SessionData.getLauncherRestoreMode();
|
||||||
spotlightContent.controller.searchMode = targetMode;
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
spotlightContent.controller.activePluginId = "";
|
spotlightContent.controller.activePluginId = "";
|
||||||
spotlightContent.controller.activePluginName = "";
|
spotlightContent.controller.activePluginName = "";
|
||||||
spotlightContent.controller.pluginFilter = "";
|
spotlightContent.controller.pluginFilter = "";
|
||||||
spotlightContent.controller.fileSearchType = "all";
|
spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
|
||||||
spotlightContent.controller.fileSearchExt = "";
|
spotlightContent.controller.fileSearchExt = "";
|
||||||
spotlightContent.controller.fileSearchFolder = "";
|
spotlightContent.controller.fileSearchFolder = "";
|
||||||
spotlightContent.controller.fileSearchSort = "score";
|
spotlightContent.controller.fileSearchSort = "score";
|
||||||
@@ -195,6 +212,7 @@ Item {
|
|||||||
function hide() {
|
function hide() {
|
||||||
if (!spotlightOpen)
|
if (!spotlightOpen)
|
||||||
return;
|
return;
|
||||||
|
spotlightContent?.closeTransientUi?.();
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
contentVisible = false;
|
contentVisible = false;
|
||||||
@@ -242,8 +260,8 @@ Item {
|
|||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
|
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps"))
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
@@ -296,12 +314,11 @@ Item {
|
|||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: clickCatcher
|
id: clickCatcher
|
||||||
screen: launcherWindow.screen
|
screen: launcherWindow.screen
|
||||||
visible: spotlightOpen || isClosing
|
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
updatesEnabled: root.useBackgroundDarken && (spotlightOpen || isClosing)
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
@@ -342,22 +359,6 @@ Item {
|
|||||||
enabled: spotlightOpen
|
enabled: spotlightOpen
|
||||||
onClicked: root.hide()
|
onClicked: root.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: backgroundDarken
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "black"
|
|
||||||
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
|
|
||||||
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
duration: Theme.modalAnimationDuration
|
|
||||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
@@ -369,7 +370,7 @@ Item {
|
|||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: launcherWindow
|
targetWindow: launcherWindow
|
||||||
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
readonly property real s: Math.min(1, modalContainer.publishedScale)
|
||||||
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2))
|
||||||
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5
|
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5
|
||||||
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
|
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
|
||||||
blurWidth: contentVisible ? modalContainer.width * s * op : 0
|
blurWidth: contentVisible ? modalContainer.width * s * op : 0
|
||||||
@@ -378,39 +379,26 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||||
if (root.useBackgroundDarken)
|
|
||||||
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.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
left: true
|
left: true
|
||||||
|
right: root.useBackgroundDarken
|
||||||
|
bottom: root.useBackgroundDarken
|
||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.margins {
|
WlrLayershell.margins {
|
||||||
left: root.windowX
|
left: root.useBackgroundDarken ? 0 : root.windowX
|
||||||
top: root.windowY
|
top: root.useBackgroundDarken ? 0 : root.windowY
|
||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
implicitWidth: root.windowWidth
|
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
|
||||||
implicitHeight: root.windowHeight
|
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
|
||||||
|
|
||||||
mask: Region {
|
mask: Region {
|
||||||
item: launcherInputMask
|
item: launcherInputMask
|
||||||
@@ -420,22 +408,48 @@ Item {
|
|||||||
id: launcherInputMask
|
id: launcherInputMask
|
||||||
visible: false
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
x: modalContainer.x
|
x: root.useBackgroundDarken ? 0 : modalContainer.x
|
||||||
y: modalContainer.y
|
y: root.useBackgroundDarken ? 0 : modalContainer.y
|
||||||
width: modalContainer.width
|
width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width
|
||||||
height: modalContainer.height
|
height: root.useBackgroundDarken ? launcherWindow.height : modalContainer.height
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.useBackgroundDarken && spotlightOpen
|
||||||
|
z: -2
|
||||||
|
onClicked: root.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: backgroundDarken
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
|
||||||
|
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
|
||||||
|
z: -3
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
duration: Theme.modalAnimationDuration
|
||||||
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: modalContainer
|
id: modalContainer
|
||||||
x: root.contentX
|
x: root.useBackgroundDarken ? root.alignedX : root.contentX
|
||||||
y: root.contentY
|
y: root.useBackgroundDarken ? root.alignedY : root.contentY
|
||||||
width: root.alignedWidth
|
width: root.alignedWidth
|
||||||
height: root.alignedHeight
|
height: root.alignedHeight
|
||||||
visible: _renderActive
|
visible: _renderActive
|
||||||
|
z: 0
|
||||||
|
|
||||||
property bool _renderActive: contentVisible
|
property bool _renderActive: contentVisible
|
||||||
property real publishedScale: contentVisible ? 1 : 0.96
|
property real publishedScale: contentVisible ? 1 : 0.96
|
||||||
|
property real publishedOpacity: contentVisible ? 1 : 0
|
||||||
|
|
||||||
opacity: contentVisible ? 1 : 0
|
opacity: contentVisible ? 1 : 0
|
||||||
scale: contentVisible ? 1 : 0.96
|
scale: contentVisible ? 1 : 0.96
|
||||||
@@ -467,6 +481,14 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on publishedOpacity {
|
||||||
|
NumberAnimation {
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
duration: Theme.modalAnimationDuration
|
||||||
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onContentVisibleChanged() {
|
function onContentVisibleChanged() {
|
||||||
@@ -514,8 +536,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
Keys.onEscapePressed: event => {
|
||||||
root.hide();
|
root.spotlightContent?.activeContextMenu?.handleKey(event);
|
||||||
|
if (!event.accepted)
|
||||||
|
root.hide();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,22 @@ FocusScope {
|
|||||||
property alias controller: controller
|
property alias controller: controller
|
||||||
property alias resultsList: resultsList
|
property alias resultsList: resultsList
|
||||||
property alias actionPanel: actionPanel
|
property alias actionPanel: actionPanel
|
||||||
|
readonly property alias activeContextMenu: contextMenu
|
||||||
|
|
||||||
property bool editMode: false
|
property bool editMode: false
|
||||||
property var editingApp: null
|
property var editingApp: null
|
||||||
property string editAppId: ""
|
property string editAppId: ""
|
||||||
|
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
|
||||||
|
readonly property real _launcherFieldAlpha: {
|
||||||
|
if (Theme.transparentBlurLayers)
|
||||||
|
return 0.28;
|
||||||
|
if (Theme.blurForegroundLayers)
|
||||||
|
return Math.max(Theme.popupTransparency, 0.62);
|
||||||
|
return Theme.popupTransparency;
|
||||||
|
}
|
||||||
|
readonly property color _launcherSearchFieldColor: Theme.withAlpha(Theme.surfaceContainerHigh, _launcherFieldAlpha)
|
||||||
|
readonly property color _launcherSearchBorderColor: Theme.withAlpha(Theme.outline, _blurActive ? 0.16 : Theme.layerOutlineOpacity)
|
||||||
|
readonly property color _launcherSearchFocusedBorderColor: Theme.withAlpha(Theme.primary, _blurActive ? 0.72 : 1.0)
|
||||||
|
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
resultsList.resetScroll();
|
resultsList.resetScroll();
|
||||||
@@ -30,6 +42,12 @@ FocusScope {
|
|||||||
searchField.forceActiveFocus();
|
searchField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeTransientUi() {
|
||||||
|
contextMenu.hide();
|
||||||
|
actionPanel.hide();
|
||||||
|
root.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
function openEditMode(app) {
|
function openEditMode(app) {
|
||||||
if (!app)
|
if (!app)
|
||||||
return;
|
return;
|
||||||
@@ -111,6 +129,21 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.parentModal
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
|
function onSpotlightOpenChanged() {
|
||||||
|
if (!root.parentModal?.spotlightOpen)
|
||||||
|
root.closeTransientUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContentVisibleChanged() {
|
||||||
|
if (!root.parentModal?.contentVisible)
|
||||||
|
root.closeTransientUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
@@ -257,13 +290,6 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Slash:
|
|
||||||
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
|
|
||||||
controller.setMode("files", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.accepted = false;
|
|
||||||
return;
|
|
||||||
default:
|
default:
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
}
|
}
|
||||||
@@ -284,7 +310,7 @@ FocusScope {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.leftMargin: root.parentModal?.borderWidth ?? 1
|
anchors.leftMargin: root.parentModal?.borderWidth ?? 1
|
||||||
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
|
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
|
||||||
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingS : (root.parentModal?.borderWidth ?? 1)
|
anchors.bottomMargin: _connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1)
|
||||||
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
|
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
|
||||||
visible: showFooter
|
visible: showFooter
|
||||||
clip: true
|
clip: true
|
||||||
@@ -293,7 +319,7 @@ FocusScope {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.topMargin: -Theme.cornerRadius
|
anchors.topMargin: -Theme.cornerRadius
|
||||||
// In connected mode the launcher provides the surface so update the toolbar for arcs
|
// In connected mode the launcher provides the surface so update the toolbar for arcs
|
||||||
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false)
|
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !root._blurActive
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
}
|
}
|
||||||
@@ -458,9 +484,11 @@ FocusScope {
|
|||||||
id: searchField
|
id: searchField
|
||||||
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
|
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
backgroundColor: root._launcherSearchFieldColor
|
||||||
normalBorderColor: Theme.outlineMedium
|
normalBorderColor: root._launcherSearchBorderColor
|
||||||
focusedBorderColor: Theme.primary
|
focusedBorderColor: root._launcherSearchFocusedBorderColor
|
||||||
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
|
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
|
||||||
leftIconSize: Theme.iconSize
|
leftIconSize: Theme.iconSize
|
||||||
leftIconColor: Theme.surfaceVariantText
|
leftIconColor: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -1,35 +1,72 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Popup {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
width: 0
|
||||||
|
height: 0
|
||||||
|
|
||||||
property var item: null
|
property var item: null
|
||||||
property var controller: null
|
property var controller: null
|
||||||
property var searchField: null
|
property var searchField: null
|
||||||
property var parentHandler: null
|
property var parentHandler: null
|
||||||
property bool allowEditActions: true
|
property bool allowEditActions: true
|
||||||
|
property real menuMargin: 8
|
||||||
|
property var targetScreen: null
|
||||||
|
property real anchorX: 0
|
||||||
|
property real anchorY: 0
|
||||||
|
property bool openState: false
|
||||||
|
property bool renderActive: false
|
||||||
|
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
|
||||||
|
|
||||||
|
readonly property real minMenuWidth: 180
|
||||||
|
readonly property real maxMenuWidth: Math.max(0, (targetScreen?.width ?? 500) - menuMargin * 2)
|
||||||
|
readonly property real maxMenuHeight: Math.max(0, (targetScreen?.height ?? 600) - menuMargin * 2)
|
||||||
|
readonly property string longestMenuText: {
|
||||||
|
let longest = "";
|
||||||
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
|
const text = menuItems[i].text || "";
|
||||||
|
if (text.length > longest.length)
|
||||||
|
longest = text;
|
||||||
|
}
|
||||||
|
return longest;
|
||||||
|
}
|
||||||
|
readonly property real naturalMenuWidth: Math.max(minMenuWidth, menuTextMetrics.width + Theme.iconSize + Theme.spacingS * 5)
|
||||||
|
readonly property real effectiveMenuWidth: Math.max(0, Math.min(maxMenuWidth, naturalMenuWidth))
|
||||||
|
readonly property real naturalMenuHeight: menuItemsHeight() + Theme.spacingS * 2
|
||||||
|
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
|
||||||
|
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
|
||||||
|
|
||||||
signal hideRequested
|
signal hideRequested
|
||||||
signal editAppRequested(var app)
|
signal editAppRequested(var app)
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: menuTextMetrics
|
||||||
|
text: root.longestMenuText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
function hasContextMenuActions(spotlightItem) {
|
function hasContextMenuActions(spotlightItem) {
|
||||||
if (!spotlightItem)
|
if (!spotlightItem)
|
||||||
return false;
|
return false;
|
||||||
if (spotlightItem.type === "app")
|
if (spotlightItem.type === "app")
|
||||||
return true;
|
return true;
|
||||||
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
||||||
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
const instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
||||||
if (!instance)
|
if (!instance)
|
||||||
return false;
|
return false;
|
||||||
if (typeof instance.getContextMenuActions !== "function")
|
if (typeof instance.getContextMenuActions !== "function")
|
||||||
return false;
|
return false;
|
||||||
var actions = instance.getContextMenuActions(spotlightItem.data);
|
const actions = instance.getContextMenuActions(spotlightItem.data);
|
||||||
return Array.isArray(actions) && actions.length > 0;
|
return Array.isArray(actions) && actions.length > 0;
|
||||||
}
|
}
|
||||||
if (spotlightItem.actions && spotlightItem.actions.length > 0)
|
if (spotlightItem.actions && spotlightItem.actions.length > 0)
|
||||||
@@ -54,13 +91,13 @@ Popup {
|
|||||||
if (!isPluginItem || !item?.pluginId)
|
if (!isPluginItem || !item?.pluginId)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
var instance = PluginService.pluginInstances[item.pluginId];
|
const instance = PluginService.pluginInstances[item.pluginId];
|
||||||
if (!instance)
|
if (!instance)
|
||||||
return [];
|
return [];
|
||||||
if (typeof instance.getContextMenuActions !== "function")
|
if (typeof instance.getContextMenuActions !== "function")
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
var actions = instance.getContextMenuActions(item.data);
|
const actions = instance.getContextMenuActions(item.data);
|
||||||
if (!Array.isArray(actions))
|
if (!Array.isArray(actions))
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
@@ -68,8 +105,8 @@ Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function executePluginAction(actionOrObj) {
|
function executePluginAction(actionOrObj) {
|
||||||
var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
|
const actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
|
||||||
var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
|
const closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
|
||||||
|
|
||||||
if (typeof actionFunc === "function")
|
if (typeof actionFunc === "function")
|
||||||
actionFunc();
|
actionFunc();
|
||||||
@@ -90,12 +127,12 @@ Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property var menuItems: {
|
readonly property var menuItems: {
|
||||||
var items = [];
|
const items = [];
|
||||||
|
|
||||||
if (isPluginItem) {
|
if (isPluginItem) {
|
||||||
var pluginActions = getPluginContextMenuActions();
|
const pluginActions = getPluginContextMenuActions();
|
||||||
for (var i = 0; i < pluginActions.length; i++) {
|
for (let i = 0; i < pluginActions.length; i++) {
|
||||||
var act = pluginActions[i];
|
const act = pluginActions[i];
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
icon: act.icon || "play_arrow",
|
icon: act.icon || "play_arrow",
|
||||||
@@ -107,8 +144,8 @@ Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
|
if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
|
||||||
for (var i = 0; i < item.actions.length; i++) {
|
for (let i = 0; i < item.actions.length; i++) {
|
||||||
var genericAct = item.actions[i];
|
const genericAct = item.actions[i];
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
icon: genericAct.icon || "play_arrow",
|
icon: genericAct.icon || "play_arrow",
|
||||||
@@ -149,8 +186,8 @@ Popup {
|
|||||||
items.push({
|
items.push({
|
||||||
type: "separator"
|
type: "separator"
|
||||||
});
|
});
|
||||||
for (var i = 0; i < item.actions.length; i++) {
|
for (let i = 0; i < item.actions.length; i++) {
|
||||||
var act = item.actions[i];
|
const act = item.actions[i];
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
icon: act.icon || "play_arrow",
|
icon: act.icon || "play_arrow",
|
||||||
@@ -183,43 +220,52 @@ Popup {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function menuItemsHeight() {
|
||||||
|
let h = 0;
|
||||||
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
|
h += menuItems[i].type === "separator" ? 5 : 32;
|
||||||
|
}
|
||||||
|
if (menuItems.length > 1)
|
||||||
|
h += menuItems.length - 1;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
function show(x, y, spotlightItem, fromKeyboard) {
|
function show(x, y, spotlightItem, fromKeyboard) {
|
||||||
if (!spotlightItem?.data)
|
if (!spotlightItem?.data)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
item = spotlightItem;
|
item = spotlightItem;
|
||||||
selectedMenuIndex = fromKeyboard ? 0 : -1;
|
selectedMenuIndex = fromKeyboard ? 0 : -1;
|
||||||
keyboardNavigation = fromKeyboard;
|
keyboardNavigation = fromKeyboard;
|
||||||
|
|
||||||
|
const modal = parentHandler?.parentModal ?? null;
|
||||||
|
const screenRef = modal?.effectiveScreen ?? parentHandler?.Window?.window?.screen ?? searchField?.Window?.window?.screen ?? null;
|
||||||
|
const screenX = screenRef?.x || 0;
|
||||||
|
const screenY = screenRef?.y || 0;
|
||||||
|
const screenRelativeX = modal ? ((modal.alignedX ?? 0) + x) : ((parentHandler ? parentHandler.mapToGlobal(x, y).x : x) - screenX);
|
||||||
|
const screenRelativeY = modal ? ((modal.alignedY ?? 0) + y) : ((parentHandler ? parentHandler.mapToGlobal(x, y).y : y) - screenY);
|
||||||
|
|
||||||
|
targetScreen = screenRef;
|
||||||
|
anchorX = screenRelativeX + 4;
|
||||||
|
anchorY = screenRelativeY + 4;
|
||||||
|
renderActive = true;
|
||||||
|
openState = true;
|
||||||
|
|
||||||
if (parentHandler)
|
if (parentHandler)
|
||||||
parentHandler.enabled = false;
|
parentHandler.enabled = false;
|
||||||
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
var parentW = parent?.width ?? 500;
|
menuFlickable.contentY = 0;
|
||||||
var parentH = parent?.height ?? 600;
|
keyboardHandler.forceActiveFocus();
|
||||||
var menuW = width > 0 ? width : 200;
|
ensureSelectedVisible();
|
||||||
var menuH = height > 0 ? height : 200;
|
|
||||||
var margin = 8;
|
|
||||||
|
|
||||||
var posX = x + 4;
|
|
||||||
var posY = y + 4;
|
|
||||||
|
|
||||||
if (posX + menuW > parentW - margin) {
|
|
||||||
posX = Math.max(margin, parentW - menuW - margin);
|
|
||||||
}
|
|
||||||
if (posY + menuH > parentH - margin) {
|
|
||||||
posY = Math.max(margin, parentH - menuH - margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
root.x = posX;
|
|
||||||
root.y = posY;
|
|
||||||
open();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
if (parentHandler)
|
if (!renderActive)
|
||||||
parentHandler.enabled = true;
|
return;
|
||||||
close();
|
openState = false;
|
||||||
|
hideRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePin() {
|
function togglePin() {
|
||||||
@@ -286,31 +332,96 @@ Popup {
|
|||||||
property bool keyboardNavigation: false
|
property bool keyboardNavigation: false
|
||||||
|
|
||||||
readonly property int visibleItemCount: {
|
readonly property int visibleItemCount: {
|
||||||
var count = 0;
|
let count = 0;
|
||||||
for (var i = 0; i < menuItems.length; i++) {
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
if (menuItems[i].type === "item")
|
if (menuItems[i].type === "item")
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKey(event) {
|
||||||
|
if (!openState)
|
||||||
|
return;
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Down:
|
||||||
|
selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
activateSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Left:
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
hide();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectNext() {
|
function selectNext() {
|
||||||
if (visibleItemCount > 0)
|
if (visibleItemCount > 0) {
|
||||||
|
keyboardNavigation = true;
|
||||||
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
|
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
|
||||||
|
ensureSelectedVisible();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPrevious() {
|
function selectPrevious() {
|
||||||
if (visibleItemCount > 0)
|
if (visibleItemCount > 0) {
|
||||||
|
keyboardNavigation = true;
|
||||||
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
|
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
|
||||||
|
ensureSelectedVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedDelegateIndex() {
|
||||||
|
let itemIndex = 0;
|
||||||
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
|
if (menuItems[i].type !== "item")
|
||||||
|
continue;
|
||||||
|
if (itemIndex === selectedMenuIndex)
|
||||||
|
return i;
|
||||||
|
itemIndex++;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSelectedVisible() {
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (!menuFlickable || !menuRepeater)
|
||||||
|
return;
|
||||||
|
const delegateIndex = selectedDelegateIndex();
|
||||||
|
if (delegateIndex < 0)
|
||||||
|
return;
|
||||||
|
const delegate = menuRepeater.itemAt(delegateIndex);
|
||||||
|
if (!delegate)
|
||||||
|
return;
|
||||||
|
const top = delegate.y;
|
||||||
|
const bottom = top + delegate.height;
|
||||||
|
const viewTop = menuFlickable.contentY;
|
||||||
|
const viewBottom = viewTop + menuFlickable.height;
|
||||||
|
if (top < viewTop) {
|
||||||
|
menuFlickable.contentY = Math.max(0, top);
|
||||||
|
} else if (bottom > viewBottom) {
|
||||||
|
menuFlickable.contentY = Math.min(Math.max(0, menuFlickable.contentHeight - menuFlickable.height), bottom - menuFlickable.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateSelected() {
|
function activateSelected() {
|
||||||
var itemIndex = 0;
|
let itemIndex = 0;
|
||||||
for (var i = 0; i < menuItems.length; i++) {
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
if (menuItems[i].type !== "item")
|
if (menuItems[i].type !== "item")
|
||||||
continue;
|
continue;
|
||||||
if (itemIndex === selectedMenuIndex) {
|
if (itemIndex === selectedMenuIndex) {
|
||||||
var menuItem = menuItems[i];
|
const menuItem = menuItems[i];
|
||||||
if (menuItem.action)
|
if (menuItem.action)
|
||||||
menuItem.action();
|
menuItem.action();
|
||||||
else if (menuItem.pluginAction)
|
else if (menuItem.pluginAction)
|
||||||
@@ -325,209 +436,233 @@ Popup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
width: menuContainer.implicitWidth
|
PanelWindow {
|
||||||
height: menuContainer.implicitHeight
|
id: menuWindow
|
||||||
padding: 0
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
modal: true
|
|
||||||
dim: false
|
|
||||||
background: Item {}
|
|
||||||
|
|
||||||
onOpened: {
|
screen: root.targetScreen
|
||||||
Qt.callLater(() => keyboardHandler.forceActiveFocus());
|
visible: root.renderActive
|
||||||
}
|
color: "transparent"
|
||||||
|
|
||||||
onClosed: {
|
WlrLayershell.namespace: "dms:launcher-context-menu"
|
||||||
if (parentHandler)
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
parentHandler.enabled = true;
|
WlrLayershell.exclusiveZone: -1
|
||||||
if (searchField?.visible) {
|
WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
Qt.callLater(() => searchField.forceActiveFocus());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enter: Transition {
|
anchors {
|
||||||
NumberAnimation {
|
top: true
|
||||||
property: "opacity"
|
left: true
|
||||||
from: 0
|
right: true
|
||||||
to: 1
|
bottom: true
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit: Transition {
|
|
||||||
NumberAnimation {
|
|
||||||
property: "opacity"
|
|
||||||
from: 1
|
|
||||||
to: 0
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Item {
|
|
||||||
id: keyboardHandler
|
|
||||||
focus: true
|
|
||||||
implicitWidth: menuContainer.implicitWidth
|
|
||||||
implicitHeight: menuContainer.implicitHeight
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
switch (event.key) {
|
|
||||||
case Qt.Key_Down:
|
|
||||||
root.selectNext();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
case Qt.Key_Up:
|
|
||||||
root.selectPrevious();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
case Qt.Key_Return:
|
|
||||||
case Qt.Key_Enter:
|
|
||||||
root.activateSelected();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
case Qt.Key_Escape:
|
|
||||||
case Qt.Key_Left:
|
|
||||||
root.hide();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
WindowBlur {
|
||||||
id: menuContainer
|
targetWindow: menuWindow
|
||||||
|
blurX: root.blurActive ? menuContainer.x : 0
|
||||||
|
blurY: root.blurActive ? menuContainer.y : 0
|
||||||
|
blurWidth: root.blurActive ? menuContainer.width : 0
|
||||||
|
blurHeight: root.blurActive ? menuContainer.height : 0
|
||||||
|
blurRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
|
z: -1
|
||||||
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
|
enabled: root.renderActive
|
||||||
color: Theme.floatingSurface
|
onClicked: root.hide()
|
||||||
radius: Theme.cornerRadius
|
}
|
||||||
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
Item {
|
||||||
|
id: keyboardHandler
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: root.openState
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Down:
|
||||||
|
root.selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
root.selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
root.activateSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
case Qt.Key_Left:
|
||||||
|
root.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
id: menuContainer
|
||||||
anchors.topMargin: 4
|
x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX))
|
||||||
anchors.leftMargin: 2
|
y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY))
|
||||||
anchors.rightMargin: -2
|
width: root.effectiveMenuWidth
|
||||||
anchors.bottomMargin: -4
|
height: root.effectiveMenuHeight
|
||||||
radius: parent.radius
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
radius: Theme.cornerRadius
|
||||||
z: -1
|
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
}
|
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||||
|
opacity: root.openState ? 1 : 0
|
||||||
|
|
||||||
Column {
|
Behavior on opacity {
|
||||||
id: menuColumn
|
NumberAnimation {
|
||||||
anchors.fill: parent
|
duration: Theme.shortDuration
|
||||||
anchors.margins: Theme.spacingS
|
easing.type: Theme.emphasizedEasing
|
||||||
spacing: 1
|
onRunningChanged: {
|
||||||
|
if (!running && !root.openState) {
|
||||||
Repeater {
|
root.renderActive = false;
|
||||||
model: root.menuItems
|
if (root.parentHandler)
|
||||||
|
root.parentHandler.enabled = true;
|
||||||
Item {
|
if (root.searchField?.visible)
|
||||||
id: menuItemDelegate
|
Qt.callLater(() => root.searchField.forceActiveFocus());
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: menuColumn.width
|
|
||||||
height: modelData.type === "separator" ? 5 : 32
|
|
||||||
|
|
||||||
readonly property int itemIndex: {
|
|
||||||
var count = 0;
|
|
||||||
for (var i = 0; i < index; i++) {
|
|
||||||
if (root.menuItems[i].type === "item")
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
visible: menuItemDelegate.modelData.type === "separator"
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: parent.height
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: menuItemDelegate.modelData.type === "item"
|
anchors.fill: parent
|
||||||
width: parent.width
|
anchors.topMargin: 4
|
||||||
height: parent.height
|
anchors.leftMargin: 2
|
||||||
radius: Theme.cornerRadius
|
anchors.rightMargin: -2
|
||||||
color: {
|
anchors.bottomMargin: -4
|
||||||
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
|
radius: parent.radius
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: menuFlickable
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: menuColumn.implicitHeight
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
interactive: root.menuScrolls
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: menuColumn
|
||||||
|
width: menuFlickable.width
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: menuRepeater
|
||||||
|
model: root.menuItems
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: menuItemDelegate
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: menuColumn.width
|
||||||
|
height: modelData.type === "separator" ? 5 : 32
|
||||||
|
|
||||||
|
readonly property int itemIndex: {
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
if (root.menuItems[i].type === "item")
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
visible: menuItemDelegate.modelData.type === "separator"
|
||||||
anchors.leftMargin: Theme.spacingS
|
width: parent.width - Theme.spacingS * 2
|
||||||
anchors.right: parent.right
|
height: parent.height
|
||||||
anchors.rightMargin: Theme.spacingS
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
color: "transparent"
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
Rectangle {
|
||||||
width: Theme.iconSize - 2
|
anchors.centerIn: parent
|
||||||
height: Theme.iconSize - 2
|
width: parent.width
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
DankIcon {
|
|
||||||
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
|
|
||||||
name: menuItemDelegate.modelData?.icon ?? ""
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
Rectangle {
|
||||||
text: menuItemDelegate.modelData.text || ""
|
visible: menuItemDelegate.modelData.type === "item"
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
width: parent.width
|
||||||
color: Theme.surfaceText
|
height: parent.height
|
||||||
font.weight: Font.Normal
|
radius: Theme.cornerRadius
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
color: {
|
||||||
elide: Text.ElideRight
|
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
|
||||||
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
|
||||||
}
|
}
|
||||||
}
|
return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||||
|
}
|
||||||
|
|
||||||
DankRipple {
|
Row {
|
||||||
id: menuItemRipple
|
anchors.left: parent.left
|
||||||
rippleColor: Theme.surfaceText
|
anchors.leftMargin: Theme.spacingS
|
||||||
cornerRadius: Theme.cornerRadius
|
anchors.right: parent.right
|
||||||
}
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
MouseArea {
|
Item {
|
||||||
id: itemMouseArea
|
width: Theme.iconSize - 2
|
||||||
anchors.fill: parent
|
height: Theme.iconSize - 2
|
||||||
hoverEnabled: true
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onEntered: {
|
DankIcon {
|
||||||
root.keyboardNavigation = false;
|
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
|
||||||
root.selectedMenuIndex = menuItemDelegate.itemIndex;
|
name: menuItemDelegate.modelData?.icon ?? ""
|
||||||
}
|
size: Theme.iconSize - 2
|
||||||
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
|
color: Theme.surfaceText
|
||||||
onClicked: {
|
opacity: 0.7
|
||||||
var menuItem = menuItemDelegate.modelData;
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
if (menuItem.action)
|
}
|
||||||
menuItem.action();
|
}
|
||||||
else if (menuItem.pluginAction)
|
|
||||||
root.executePluginAction(menuItem.pluginAction);
|
StyledText {
|
||||||
else if (menuItem.launcherActionData)
|
text: menuItemDelegate.modelData.text || ""
|
||||||
root.executeLauncherAction(menuItem.launcherActionData);
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
else if (menuItem.actionData)
|
color: Theme.surfaceText
|
||||||
root.executeDesktopAction(menuItem.actionData);
|
font.weight: Font.Normal
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankRipple {
|
||||||
|
id: menuItemRipple
|
||||||
|
rippleColor: Theme.surfaceText
|
||||||
|
cornerRadius: Theme.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onEntered: {
|
||||||
|
root.keyboardNavigation = false;
|
||||||
|
root.selectedMenuIndex = menuItemDelegate.itemIndex;
|
||||||
|
}
|
||||||
|
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
|
||||||
|
onClicked: {
|
||||||
|
const menuItem = menuItemDelegate.modelData;
|
||||||
|
if (menuItem.action)
|
||||||
|
menuItem.action();
|
||||||
|
else if (menuItem.pluginAction)
|
||||||
|
root.executePluginAction(menuItem.pluginAction);
|
||||||
|
else if (menuItem.launcherActionData)
|
||||||
|
root.executeLauncherAction(menuItem.launcherActionData);
|
||||||
|
else if (menuItem.actionData)
|
||||||
|
root.executeDesktopAction(menuItem.actionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ FocusScope {
|
|||||||
property var parentModal: null
|
property var parentModal: null
|
||||||
property alias searchField: searchInput
|
property alias searchField: searchInput
|
||||||
property alias controller: searchController
|
property alias controller: searchController
|
||||||
|
readonly property alias activeContextMenu: contextMenu
|
||||||
|
|
||||||
readonly property bool _hasQuery: searchInput.text.length > 0
|
readonly property bool _hasQuery: searchInput.text.length > 0
|
||||||
readonly property real _searchBarH: 56
|
readonly property real _searchBarH: 56
|
||||||
readonly property real _surfaceInset: BlurService.enabled ? (_hasQuery ? Theme.spacingS : Theme.spacingXS) : 0
|
readonly property real _searchAreaH: _searchBarH
|
||||||
readonly property real _searchAreaH: _searchBarH + _surfaceInset * 2
|
|
||||||
readonly property real _statusH: 92
|
readonly property real _statusH: 92
|
||||||
readonly property real _rowH: 64
|
readonly property real _rowH: 64
|
||||||
readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
|
readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
|
||||||
@@ -25,13 +25,34 @@ FocusScope {
|
|||||||
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
|
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
|
||||||
readonly property int _fastDuration: 90
|
readonly property int _fastDuration: 90
|
||||||
readonly property int _resizeDuration: 110
|
readonly property int _resizeDuration: 110
|
||||||
|
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
|
||||||
|
readonly property real _searchSurfaceAlpha: {
|
||||||
|
if (Theme.transparentBlurLayers)
|
||||||
|
return _hasQuery ? 0.34 : 0.28;
|
||||||
|
if (Theme.blurForegroundLayers)
|
||||||
|
return Math.max(Theme.popupTransparency, _hasQuery ? 0.68 : 0.74);
|
||||||
|
return _hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9);
|
||||||
|
}
|
||||||
|
readonly property color _searchSurfaceColor: Theme.withAlpha(_hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, _searchSurfaceAlpha)
|
||||||
|
readonly property color _searchWellColor: {
|
||||||
|
if (searchInput.activeFocus)
|
||||||
|
return Theme.withAlpha(Theme.primaryContainer, Theme.transparentBlurLayers ? 0.42 : 1.0);
|
||||||
|
if (Theme.transparentBlurLayers)
|
||||||
|
return Theme.ccPillInactiveBg;
|
||||||
|
return Theme.surfaceContainer;
|
||||||
|
}
|
||||||
|
|
||||||
implicitHeight: _searchAreaH + (_resultsH > 0 ? 1 + _resultsH : 0)
|
implicitHeight: _searchAreaH + _resultsH
|
||||||
|
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
resultsList.resetScroll();
|
resultsList.resetScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeTransientUi() {
|
||||||
|
contextMenu.hide();
|
||||||
|
root.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
function _buildRows() {
|
function _buildRows() {
|
||||||
const flat = searchController.flatModel || [];
|
const flat = searchController.flatModel || [];
|
||||||
const sections = searchController.sections || [];
|
const sections = searchController.sections || [];
|
||||||
@@ -122,13 +143,11 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Qt.Key_Tab:
|
case Qt.Key_Tab:
|
||||||
if (_hasQuery)
|
_cycleCategory(false);
|
||||||
_cycleCategory(false);
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Backtab:
|
case Qt.Key_Backtab:
|
||||||
if (_hasQuery)
|
_cycleCategory(true);
|
||||||
_cycleCategory(true);
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
return;
|
||||||
case Qt.Key_Return:
|
case Qt.Key_Return:
|
||||||
@@ -177,13 +196,6 @@ FocusScope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Qt.Key_Slash:
|
|
||||||
if (event.modifiers === Qt.NoModifier && searchInput.text.length === 0) {
|
|
||||||
searchController.setMode("files", true);
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
event.accepted = false;
|
event.accepted = false;
|
||||||
@@ -193,6 +205,7 @@ FocusScope {
|
|||||||
id: searchController
|
id: searchController
|
||||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||||
viewModeContext: "spotlight"
|
viewModeContext: "spotlight"
|
||||||
|
forceLinearNavigation: true
|
||||||
|
|
||||||
onItemExecuted: {
|
onItemExecuted: {
|
||||||
root.parentModal?.hide();
|
root.parentModal?.hide();
|
||||||
@@ -210,10 +223,25 @@ FocusScope {
|
|||||||
allowEditActions: false
|
allowEditActions: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.parentModal
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
|
function onSpotlightOpenChanged() {
|
||||||
|
if (!root.parentModal?.spotlightOpen)
|
||||||
|
root.closeTransientUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContentVisibleChanged() {
|
||||||
|
if (!root.parentModal?.contentVisible)
|
||||||
|
root.closeTransientUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: searchController
|
target: searchController
|
||||||
function onModeChanged(mode) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
if (searchController.autoSwitchedToFiles)
|
if (!userInitiated || !SettingsData.rememberLastMode)
|
||||||
return;
|
return;
|
||||||
SessionData.setLauncherLastMode(mode);
|
SessionData.setLauncherLastMode(mode);
|
||||||
}
|
}
|
||||||
@@ -233,11 +261,8 @@ FocusScope {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: searchBarSurface
|
id: searchBarSurface
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root._surfaceInset
|
radius: Theme.cornerRadius
|
||||||
radius: height / 2
|
color: root._searchSurfaceColor
|
||||||
color: Theme.withAlpha(root._hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, root._hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9))
|
|
||||||
border.color: BlurService.enabled && !root._hasQuery ? Theme.withAlpha(Theme.outline, 0.08) : "transparent"
|
|
||||||
border.width: BlurService.enabled && !root._hasQuery ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
@@ -254,7 +279,7 @@ FocusScope {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer
|
color: root._searchWellColor
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -273,8 +298,8 @@ FocusScope {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: categoryRow
|
id: categoryRow
|
||||||
|
visible: SettingsData.spotlightBarShowModeChips || root._hasQuery
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingXS
|
||||||
visible: root._hasQuery
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
@@ -380,28 +405,9 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.top: searchBarItem.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.leftMargin: root._surfaceInset
|
|
||||||
anchors.rightMargin: root._surfaceInset
|
|
||||||
height: 1
|
|
||||||
color: Theme.outlineMedium
|
|
||||||
opacity: root._resultsH > 0 ? 0.55 : 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root._fastDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: resultsContainer
|
id: resultsContainer
|
||||||
anchors.top: searchBarItem.bottom
|
anchors.top: searchBarItem.bottom
|
||||||
anchors.topMargin: 1
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
clip: true
|
clip: true
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Item {
|
|||||||
property var controller: null
|
property var controller: null
|
||||||
property bool hasQuery: false
|
property bool hasQuery: false
|
||||||
property var rows: []
|
property var rows: []
|
||||||
|
readonly property real bottomInset: Theme.spacingS
|
||||||
|
|
||||||
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
||||||
|
|
||||||
@@ -53,7 +54,11 @@ Item {
|
|||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
id: mainListView
|
id: mainListView
|
||||||
anchors.fill: parent
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: root.bottomInset
|
||||||
clip: true
|
clip: true
|
||||||
visible: root.rows.length > 0
|
visible: root.rows.length > 0
|
||||||
|
|
||||||
@@ -64,11 +69,6 @@ Item {
|
|||||||
objectProp: "_rowId"
|
objectProp: "_rowId"
|
||||||
}
|
}
|
||||||
|
|
||||||
add: null
|
|
||||||
remove: null
|
|
||||||
displaced: null
|
|
||||||
move: null
|
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: delegateRoot
|
id: delegateRoot
|
||||||
required property var modelData
|
required property var modelData
|
||||||
@@ -103,7 +103,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: root.bottomInset
|
||||||
visible: root.hasQuery && root.rows.length === 0
|
visible: root.hasQuery && root.rows.length === 0
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ DankModal {
|
|||||||
executeAction(action);
|
executeAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signal switchUserRequested
|
||||||
|
|
||||||
function executeAction(action) {
|
function executeAction(action) {
|
||||||
if (action === "lock") {
|
if (action === "lock") {
|
||||||
close();
|
close();
|
||||||
@@ -92,6 +94,11 @@ DankModal {
|
|||||||
Quickshell.execDetached(["dms", "restart"]);
|
Quickshell.execDetached(["dms", "restart"]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (action === "switchuser") {
|
||||||
|
close();
|
||||||
|
switchUserRequested();
|
||||||
|
return;
|
||||||
|
}
|
||||||
close();
|
close();
|
||||||
root.powerActionRequested(action, "", "");
|
root.powerActionRequested(action, "", "");
|
||||||
}
|
}
|
||||||
@@ -216,6 +223,12 @@ DankModal {
|
|||||||
"label": I18n.tr("Restart DMS"),
|
"label": I18n.tr("Restart DMS"),
|
||||||
"key": "D"
|
"key": "D"
|
||||||
};
|
};
|
||||||
|
case "switchuser":
|
||||||
|
return {
|
||||||
|
"icon": "switch_account",
|
||||||
|
"label": I18n.tr("Switch User"),
|
||||||
|
"key": "U"
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
"icon": "help",
|
"icon": "help",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ FocusScope {
|
|||||||
|
|
||||||
sourceComponent: KeybindsTab {
|
sourceComponent: KeybindsTab {
|
||||||
parentModal: root.parentModal
|
parentModal: root.parentModal
|
||||||
|
requestedSearchQuery: root.parentModal?.keybindSearchQuery ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
@@ -554,5 +555,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: usersLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 35
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: UsersTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ FloatingWindow {
|
|||||||
property bool isCompactMode: width < 700
|
property bool isCompactMode: width < 700
|
||||||
property bool menuVisible: !isCompactMode
|
property bool menuVisible: !isCompactMode
|
||||||
property bool enableAnimations: true
|
property bool enableAnimations: true
|
||||||
|
property string keybindSearchQuery: ""
|
||||||
|
|
||||||
signal closingModal
|
signal closingModal
|
||||||
|
|
||||||
@@ -73,6 +74,11 @@ FloatingWindow {
|
|||||||
return sidebar.resolveTabIndex(tabName);
|
return sidebar.resolveTabIndex(tabName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showKeybindsSearch(query: string) {
|
||||||
|
keybindSearchQuery = query || "";
|
||||||
|
showWithTabName("keybinds");
|
||||||
|
}
|
||||||
|
|
||||||
function toggleMenu() {
|
function toggleMenu() {
|
||||||
enableAnimations = true;
|
enableAnimations = true;
|
||||||
menuVisible = !menuVisible;
|
menuVisible = !menuVisible;
|
||||||
|
|||||||
@@ -293,6 +293,12 @@ Rectangle {
|
|||||||
"tabIndex": 20,
|
"tabIndex": 20,
|
||||||
"updaterOnly": true
|
"updaterOnly": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "users",
|
||||||
|
"text": I18n.tr("Users"),
|
||||||
|
"icon": "manage_accounts",
|
||||||
|
"tabIndex": 35
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "window_rules",
|
"id": "window_rules",
|
||||||
"text": I18n.tr("Window Rules"),
|
"text": I18n.tr("Window Rules"),
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
DankModal {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool lockOnSwitch: false
|
||||||
|
|
||||||
|
function showFromPowerMenu() {
|
||||||
|
root.lockOnSwitch = false;
|
||||||
|
SessionsService.refresh();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFromLockScreen() {
|
||||||
|
root.lockOnSwitch = true;
|
||||||
|
SessionsService.refresh();
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatTty(s) {
|
||||||
|
if (s.tty && s.tty.length > 0)
|
||||||
|
return s.tty;
|
||||||
|
if (s.seat && s.seat.length > 0)
|
||||||
|
return s.seat;
|
||||||
|
return I18n.tr("remote");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatType(s) {
|
||||||
|
if (!s.type || s.type.length === 0)
|
||||||
|
return "";
|
||||||
|
switch (s.type) {
|
||||||
|
case "wayland":
|
||||||
|
return "Wayland";
|
||||||
|
case "x11":
|
||||||
|
return "X11";
|
||||||
|
case "tty":
|
||||||
|
return "TTY";
|
||||||
|
default:
|
||||||
|
return s.type.charAt(0).toUpperCase() + s.type.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _doSwitch(sessionId, username) {
|
||||||
|
if (root.lockOnSwitch && typeof SessionService !== "undefined" && SessionService.loginctlAvailable)
|
||||||
|
SessionService.lock();
|
||||||
|
SessionsService.activate(sessionId, null);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
layerNamespace: "dms:switch-user-modal"
|
||||||
|
shouldBeVisible: false
|
||||||
|
allowStacking: true
|
||||||
|
modalWidth: 420
|
||||||
|
modalHeight: contentLoader.item ? Math.min(540, contentLoader.item.implicitHeight + Theme.spacingM * 2) : 320
|
||||||
|
enableShadow: true
|
||||||
|
shouldHaveFocus: true
|
||||||
|
onBackgroundClicked: close()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionsService
|
||||||
|
function onSwitchRequested() {
|
||||||
|
root.showFromPowerMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: mainColumn.implicitHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.rightMargin: Theme.spacingL
|
||||||
|
anchors.topMargin: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "switch_account"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Switch User")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Select an active session to switch to. The current session stays running in the background.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
visible: SessionsService.otherSessions().length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SessionsService.otherSessions().length > 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: SessionsService.otherSessions()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sessionRow
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: sessionMouse.containsMouse ? Theme.surfacePressed : Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "account_circle"
|
||||||
|
size: Theme.iconSize + 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - 4 - chevron.width - Theme.spacingM * 2
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: sessionRow.modelData.username
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
const tty = root._formatTty(sessionRow.modelData);
|
||||||
|
const type = root._formatType(sessionRow.modelData);
|
||||||
|
const parts = [];
|
||||||
|
if (type)
|
||||||
|
parts.push(type);
|
||||||
|
parts.push(I18n.tr("session %1").arg(sessionRow.modelData.sessionId));
|
||||||
|
if (tty)
|
||||||
|
parts.push(tty);
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: chevron
|
||||||
|
name: "chevron_right"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sessionMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root._doSwitch(sessionRow.modelData.sessionId, sessionRow.modelData.username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: SessionsService.otherSessions().length === 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: bodyCol.implicitHeight + Theme.spacingM * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "info"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bodyCol
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("No other active sessions on this seat")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("To sign in as a different user, log out and pick the account from the greeter. Creating a fresh session in parallel needs a multi-session greeter (greetd-flexiserver / GDM / LightDM).")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
layoutDirection: Qt.RightToLeft
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Close")
|
||||||
|
backgroundColor: Theme.surfaceVariantAlpha
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
visible: SessionsService.otherSessions().length === 0 && !root.lockOnSwitch
|
||||||
|
text: I18n.tr("Log out")
|
||||||
|
iconName: "logout"
|
||||||
|
backgroundColor: Theme.primary
|
||||||
|
textColor: Theme.primaryText
|
||||||
|
onClicked: {
|
||||||
|
if (typeof SessionService !== "undefined")
|
||||||
|
SessionService.logout();
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,21 +59,19 @@ Item {
|
|||||||
ignoreUnknownSignals: true
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
function onDeviceNameChanged(newDeviceName) {
|
function onDeviceNameChanged(newDeviceName) {
|
||||||
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
|
if (!root.expandedWidgetData || root.expandedWidgetData.id !== "brightnessSlider") {
|
||||||
const widgets = SettingsData.controlCenterWidgets || [];
|
return;
|
||||||
const newWidgets = widgets.map(w => {
|
|
||||||
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
|
|
||||||
const updatedWidget = Object.assign({}, w);
|
|
||||||
updatedWidget.deviceName = newDeviceName;
|
|
||||||
return updatedWidget;
|
|
||||||
}
|
|
||||||
return w;
|
|
||||||
});
|
|
||||||
SettingsData.set("controlCenterWidgets", newWidgets);
|
|
||||||
if (root.collapseCallback) {
|
|
||||||
root.collapseCallback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || [];
|
||||||
|
const newWidgets = widgets.map(w => {
|
||||||
|
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
|
||||||
|
const updatedWidget = Object.assign({}, w);
|
||||||
|
updatedWidget.deviceName = newDeviceName;
|
||||||
|
return updatedWidget;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
});
|
||||||
|
SettingsData.set("controlCenterWidgets", newWidgets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -301,12 +301,22 @@ Column {
|
|||||||
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 60
|
height: 60
|
||||||
|
iconBlinking: {
|
||||||
|
const id = widgetData.id || "";
|
||||||
|
if (id === "wifi")
|
||||||
|
return NetworkService.isWifiConnecting;
|
||||||
|
if (id === "bluetooth")
|
||||||
|
return BluetoothService.connecting;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
iconName: {
|
iconName: {
|
||||||
switch (widgetData.id || "") {
|
switch (widgetData.id || "") {
|
||||||
case "wifi":
|
case "wifi":
|
||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return "sync";
|
return "sync";
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return NetworkService.wifiSignalIcon;
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -360,6 +370,8 @@ Column {
|
|||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
|
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return NetworkService.connectingSSID || I18n.tr("Connecting...", "network status");
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -400,6 +412,8 @@ Column {
|
|||||||
{
|
{
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return I18n.tr("Please wait...", "network status");
|
return I18n.tr("Please wait...", "network status");
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return I18n.tr("Connecting...", "network status");
|
||||||
|
|
||||||
const status = NetworkService.networkStatus;
|
const status = NetworkService.networkStatus;
|
||||||
if (status === "ethernet")
|
if (status === "ethernet")
|
||||||
@@ -422,6 +436,8 @@ Column {
|
|||||||
return I18n.tr("No adapters", "bluetooth status");
|
return I18n.tr("No adapters", "bluetooth status");
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
return I18n.tr("Off", "bluetooth status");
|
return I18n.tr("Off", "bluetooth status");
|
||||||
|
if (BluetoothService.connecting)
|
||||||
|
return I18n.tr("Connecting...", "bluetooth status");
|
||||||
const primaryDevice = (() => {
|
const primaryDevice = (() => {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -23,79 +23,103 @@ Rectangle {
|
|||||||
if (!screenName)
|
if (!screenName)
|
||||||
return "";
|
return "";
|
||||||
const screen = Quickshell.screens.find(s => s.name === screenName);
|
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||||
if (screen) {
|
if (screen)
|
||||||
return SettingsData.getScreenDisplayName(screen);
|
return SettingsData.getScreenDisplayName(screen);
|
||||||
}
|
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0)
|
||||||
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0) {
|
|
||||||
return screenModel;
|
return screenModel;
|
||||||
}
|
|
||||||
return screenName;
|
return screenName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDeviceName() {
|
function resolveCurrentDevice() {
|
||||||
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
|
const devices = DisplayService.devices || [];
|
||||||
|
if (!DisplayService.brightnessAvailable || devices.length === 0)
|
||||||
return "";
|
return "";
|
||||||
}
|
|
||||||
|
|
||||||
const pinKey = getScreenPinKey();
|
const pinKey = getScreenPinKey();
|
||||||
if (pinKey.length > 0) {
|
if (pinKey.length > 0) {
|
||||||
const pins = SettingsData.brightnessDevicePins || {};
|
const pins = SettingsData.brightnessDevicePins || {};
|
||||||
const pinnedDevice = pins[pinKey];
|
const pinnedDevice = pins[pinKey];
|
||||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||||
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
|
const found = devices.find(d => d.name === pinnedDevice);
|
||||||
if (found)
|
if (found)
|
||||||
return found.name;
|
return found.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instanceId) {
|
||||||
|
const widgets = SettingsData.controlCenterWidgets || [];
|
||||||
|
const widget = widgets.find(w => w.id === "brightnessSlider" && w.instanceId === instanceId);
|
||||||
|
if (widget && typeof widget.deviceName === "string" && widget.deviceName.length > 0) {
|
||||||
|
const found = devices.find(d => d.name === widget.deviceName);
|
||||||
|
if (found)
|
||||||
|
return found.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DisplayService.currentDevice) {
|
||||||
|
const found = devices.find(d => d.name === DisplayService.currentDevice);
|
||||||
|
if (found)
|
||||||
|
return found.name;
|
||||||
|
}
|
||||||
|
|
||||||
if (initialDeviceName && initialDeviceName.length > 0) {
|
if (initialDeviceName && initialDeviceName.length > 0) {
|
||||||
const found = DisplayService.devices.find(dev => dev.name === initialDeviceName);
|
const found = devices.find(d => d.name === initialDeviceName);
|
||||||
if (found)
|
if (found)
|
||||||
return found.name;
|
return found.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDeviceNameFromService = DisplayService.currentDevice;
|
const backlight = devices.find(d => d.class === "backlight");
|
||||||
if (currentDeviceNameFromService) {
|
|
||||||
const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService);
|
|
||||||
if (found)
|
|
||||||
return found.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backlight = DisplayService.devices.find(d => d.class === "backlight");
|
|
||||||
if (backlight)
|
if (backlight)
|
||||||
return backlight.name;
|
return backlight.name;
|
||||||
|
|
||||||
const ddc = DisplayService.devices.find(d => d.class === "ddc");
|
const ddc = devices.find(d => d.class === "ddc");
|
||||||
if (ddc)
|
if (ddc)
|
||||||
return ddc.name;
|
return ddc.name;
|
||||||
|
|
||||||
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "";
|
return devices[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDevice(deviceName) {
|
||||||
|
if (!deviceName || deviceName === root.currentDeviceName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pinKey = getScreenPinKey();
|
||||||
|
if (pinKey.length > 0) {
|
||||||
|
const pins = SettingsData.brightnessDevicePins || {};
|
||||||
|
const existing = pins[pinKey];
|
||||||
|
if (existing && existing !== deviceName) {
|
||||||
|
const next = JSON.parse(JSON.stringify(pins));
|
||||||
|
delete next[pinKey];
|
||||||
|
SettingsData.set("brightnessDevicePins", next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.currentDeviceName = deviceName;
|
||||||
|
DisplayService.setCurrentDevice(deviceName, true);
|
||||||
|
Qt.callLater(() => root.deviceNameChanged(deviceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
currentDeviceName = resolveDeviceName();
|
root.currentDeviceName = resolveCurrentDevice();
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool isPinnedToScreen: {
|
function isDevicePinnedToScreen(deviceName) {
|
||||||
const pinKey = getScreenPinKey();
|
const pinKey = getScreenPinKey();
|
||||||
if (!pinKey || pinKey.length === 0)
|
if (!pinKey || !deviceName)
|
||||||
return false;
|
return false;
|
||||||
const pins = SettingsData.brightnessDevicePins || {};
|
const pins = SettingsData.brightnessDevicePins || {};
|
||||||
return pins[pinKey] === currentDeviceName;
|
return pins[pinKey] === deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePinToScreen() {
|
function togglePinForDevice(deviceName) {
|
||||||
const pinKey = getScreenPinKey();
|
const pinKey = getScreenPinKey();
|
||||||
if (!pinKey || pinKey.length === 0 || !currentDeviceName || currentDeviceName.length === 0)
|
if (!pinKey || !deviceName)
|
||||||
return;
|
return;
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
|
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
|
||||||
|
if (pins[pinKey] === deviceName) {
|
||||||
if (isPinnedToScreen) {
|
|
||||||
delete pins[pinKey];
|
delete pins[pinKey];
|
||||||
} else {
|
} else {
|
||||||
pins[pinKey] = currentDeviceName;
|
pins[pinKey] = deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsData.set("brightnessDevicePins", pins);
|
SettingsData.set("brightnessDevicePins", pins);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,18 +177,23 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: monitorHeader
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 40
|
height: 40
|
||||||
visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
|
visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
|
|
||||||
|
property bool currentDevicePinned: root.isDevicePinnedToScreen(currentDeviceName)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.right: globalPinButton.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
@@ -180,47 +209,51 @@ Rectangle {
|
|||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: globalPinButton
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: pinRow.width + Theme.spacingS * 2
|
width: globalPinRow.width + Theme.spacingS * 2
|
||||||
height: 28
|
height: 28
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
color: isPinnedToScreen ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
color: monitorHeader.currentDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceText, 0.05)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: pinRow
|
id: globalPinRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: 4
|
spacing: 4
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: isPinnedToScreen ? "push_pin" : "push_pin"
|
name: "push_pin"
|
||||||
size: 16
|
size: 16
|
||||||
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
|
color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: isPinnedToScreen ? I18n.tr("Pinned") : I18n.tr("Pin")
|
text: monitorHeader.currentDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
|
color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
id: pinRipple
|
id: globalPinRipple
|
||||||
cornerRadius: parent.radius
|
cornerRadius: parent.radius
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
|
enabled: currentDeviceName && currentDeviceName.length > 0
|
||||||
onClicked: root.togglePinToScreen()
|
onPressed: mouse => globalPinRipple.trigger(mouse.x, mouse.y)
|
||||||
|
onClicked: root.togglePinForDevice(currentDeviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,9 +262,17 @@ Rectangle {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: DisplayService.devices || []
|
model: DisplayService.devices || []
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
|
id: deviceCard
|
||||||
|
|
||||||
required property var modelData
|
required property var modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
|
||||||
|
readonly property bool devicePinnedHere: {
|
||||||
|
SettingsData.brightnessDevicePins;
|
||||||
|
return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
|
||||||
|
}
|
||||||
|
|
||||||
property real deviceBrightness: {
|
property real deviceBrightness: {
|
||||||
DisplayService.brightnessVersion;
|
DisplayService.brightnessVersion;
|
||||||
return DisplayService.getDeviceBrightness(modelData.name);
|
return DisplayService.getDeviceBrightness(modelData.name);
|
||||||
@@ -241,8 +282,8 @@ Rectangle {
|
|||||||
height: 100
|
height: 100
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: selected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
border.width: modelData.name === currentDeviceName ? 2 : 0
|
border.width: selected ? 2 : 0
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -251,10 +292,12 @@ Rectangle {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, exponentControls.height)
|
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, rightControls.height)
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
anchors.right: rightControls.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
@@ -281,7 +324,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
|
color: deviceCard.selected ? Theme.primary : Theme.surfaceText
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +339,7 @@ Rectangle {
|
|||||||
Column {
|
Column {
|
||||||
id: deviceInfoColumn
|
id: deviceInfoColumn
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: parent.parent.width - deviceIconColumn.width - exponentControls.width - Theme.spacingM * 3
|
width: parent.width - deviceIconColumn.width - Theme.spacingM
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
@@ -309,7 +352,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
|
font.weight: deviceCard.selected ? Font.Medium : Font.Normal
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
width: parent.width
|
width: parent.width
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
@@ -345,80 +388,107 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: exponentControls
|
id: rightControls
|
||||||
width: 140
|
|
||||||
height: 28
|
height: 28
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingS
|
||||||
visible: SessionData.getBrightnessExponential(modelData.name)
|
|
||||||
z: 1
|
z: 1
|
||||||
|
|
||||||
StyledRect {
|
Row {
|
||||||
width: 28
|
id: exponentControls
|
||||||
height: 28
|
height: 28
|
||||||
radius: Theme.cornerRadius
|
spacing: Theme.spacingXS
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
visible: SessionData.getBrightnessExponential(modelData.name)
|
||||||
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
|
|
||||||
|
|
||||||
DankIcon {
|
StyledRect {
|
||||||
anchors.centerIn: parent
|
width: 28
|
||||||
name: "remove"
|
height: 28
|
||||||
size: 14
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceText
|
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
|
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "remove"
|
||||||
|
size: 14
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
stateColor: Theme.primary
|
||||||
|
cornerRadius: parent.radius
|
||||||
|
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
|
||||||
|
onClicked: {
|
||||||
|
const current = SessionData.getBrightnessExponent(modelData.name);
|
||||||
|
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
|
||||||
|
SessionData.setBrightnessExponent(modelData.name, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StyledRect {
|
||||||
stateColor: Theme.primary
|
width: 50
|
||||||
cornerRadius: parent.radius
|
height: 28
|
||||||
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
|
radius: Theme.cornerRadius
|
||||||
onClicked: {
|
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
const current = SessionData.getBrightnessExponent(modelData.name);
|
border.width: 0
|
||||||
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
|
|
||||||
SessionData.setBrightnessExponent(modelData.name, newValue);
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
|
opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "add"
|
||||||
|
size: 14
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
stateColor: Theme.primary
|
||||||
|
cornerRadius: parent.radius
|
||||||
|
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
|
||||||
|
onClicked: {
|
||||||
|
const current = SessionData.getBrightnessExponent(modelData.name);
|
||||||
|
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
|
||||||
|
SessionData.setBrightnessExponent(modelData.name, newValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledRect {
|
StyledRect {
|
||||||
width: 50
|
id: pinButton
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: 28
|
width: 28
|
||||||
height: 28
|
height: 28
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
visible: root.screenName && root.screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
|
||||||
opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4
|
color: devicePinnedHere ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: "add"
|
name: "push_pin"
|
||||||
size: 14
|
size: 14
|
||||||
color: Theme.surfaceText
|
color: devicePinnedHere ? Theme.primary : Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
stateColor: Theme.primary
|
stateColor: Theme.primary
|
||||||
cornerRadius: parent.radius
|
cornerRadius: parent.radius
|
||||||
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
|
onClicked: root.togglePinForDevice(modelData.name)
|
||||||
onClicked: {
|
|
||||||
const current = SessionData.getBrightnessExponent(modelData.name);
|
|
||||||
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
|
|
||||||
SessionData.setBrightnessExponent(modelData.name, newValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,22 +544,11 @@ Rectangle {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.bottomMargin: 28
|
anchors.bottomMargin: 28
|
||||||
anchors.rightMargin: SessionData.getBrightnessExponential(modelData.name) ? 145 : 0
|
anchors.rightMargin: rightControls.width + Theme.spacingS
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y)
|
onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y)
|
||||||
onClicked: {
|
onClicked: root.selectDevice(modelData.name)
|
||||||
const pinKey = root.getScreenPinKey();
|
|
||||||
if (pinKey.length > 0 && modelData.name !== currentDeviceName) {
|
|
||||||
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
|
|
||||||
if (pins[pinKey]) {
|
|
||||||
delete pins[pinKey];
|
|
||||||
SettingsData.set("brightnessDevicePins", pins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentDeviceName = modelData.name;
|
|
||||||
deviceNameChanged(modelData.name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,7 +541,11 @@ Rectangle {
|
|||||||
return -1;
|
return -1;
|
||||||
if (b.ssid === ssid)
|
if (b.ssid === ssid)
|
||||||
return 1;
|
return 1;
|
||||||
return b.signal - a.signal;
|
const aBucket = Math.floor((a.signal || 0) / 25);
|
||||||
|
const bBucket = Math.floor((b.signal || 0) / 25);
|
||||||
|
if (aBucket !== bBucket)
|
||||||
|
return bBucket - aBucket;
|
||||||
|
return (a.ssid || "").localeCompare(b.ssid || "");
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -31,8 +32,10 @@ Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (screenName && screenName.length > 0) {
|
if (screenName && screenName.length > 0) {
|
||||||
|
const screen = Quickshell.screens.find(s => s.name === screenName);
|
||||||
|
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
|
||||||
const pins = SettingsData.brightnessDevicePins || {};
|
const pins = SettingsData.brightnessDevicePins || {};
|
||||||
const pinnedDevice = pins[screenName];
|
const pinnedDevice = pins[pinKey];
|
||||||
if (pinnedDevice && pinnedDevice.length > 0) {
|
if (pinnedDevice && pinnedDevice.length > 0) {
|
||||||
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
|
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
|
||||||
if (found) {
|
if (found) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Rectangle {
|
|||||||
|
|
||||||
property string iconName: ""
|
property string iconName: ""
|
||||||
property color iconColor: Theme.surfaceText
|
property color iconColor: Theme.surfaceText
|
||||||
|
property bool iconBlinking: false
|
||||||
property string primaryText: ""
|
property string primaryText: ""
|
||||||
property string secondaryText: ""
|
property string secondaryText: ""
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
@@ -109,10 +110,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: pillIcon
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: iconName
|
name: iconName
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: isActive ? _tileIconActive : _tileIconInactive
|
color: isActive ? _tileIconActive : _tileIconInactive
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: pillIcon
|
||||||
|
running: root.iconBlinking
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ Item {
|
|||||||
required property var axis
|
required property var axis
|
||||||
required property var barConfig
|
required property var barConfig
|
||||||
|
|
||||||
visible: !SettingsData.frameEnabled
|
readonly property bool frameShapesBar: SettingsData.frameEnabled && barWindow.usesFrameBarChrome
|
||||||
|
|
||||||
|
visible: !frameShapesBar
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
readonly property bool gothEnabled: (barConfig?.gothCornersEnabled ?? false) && !barWindow.hasMaximizedToplevel
|
readonly property bool gothEnabled: (barConfig?.gothCornersEnabled ?? false) && !(barWindow.flattenForMaximizedWindow && barWindow.hasMaximizedToplevel)
|
||||||
anchors.leftMargin: -(gothEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
|
anchors.leftMargin: -(gothEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
|
||||||
anchors.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
|
anchors.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
|
||||||
anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
|
||||||
@@ -39,11 +41,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property real rt: {
|
property real rt: {
|
||||||
if (SettingsData.frameEnabled)
|
if (frameShapesBar)
|
||||||
return SettingsData.frameRounding;
|
return SettingsData.frameRounding;
|
||||||
if (barConfig?.squareCorners ?? false)
|
if (barConfig?.squareCorners ?? false)
|
||||||
return 0;
|
return 0;
|
||||||
if (barWindow.hasMaximizedToplevel)
|
if (barWindow.flattenForMaximizedWindow && barWindow.hasMaximizedToplevel)
|
||||||
return 0;
|
return 0;
|
||||||
return Theme.cornerRadius;
|
return Theme.cornerRadius;
|
||||||
}
|
}
|
||||||
@@ -113,9 +115,32 @@ Item {
|
|||||||
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
||||||
readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
||||||
|
|
||||||
readonly property string mainPath: generatePathForPosition(width, height)
|
readonly property string mainPath: {
|
||||||
readonly property string borderFullPath: generateBorderFullPath(width, height)
|
frameShapesBar;
|
||||||
readonly property string borderEdgePath: generateBorderEdgePath(width, height)
|
rt;
|
||||||
|
wing;
|
||||||
|
barWindow.flattenForMaximizedWindow;
|
||||||
|
barWindow.hasMaximizedToplevel;
|
||||||
|
width;
|
||||||
|
height;
|
||||||
|
return generatePathForPosition(width, height);
|
||||||
|
}
|
||||||
|
readonly property string borderFullPath: {
|
||||||
|
frameShapesBar;
|
||||||
|
rt;
|
||||||
|
wing;
|
||||||
|
width;
|
||||||
|
height;
|
||||||
|
return generateBorderFullPath(width, height);
|
||||||
|
}
|
||||||
|
readonly property string borderEdgePath: {
|
||||||
|
frameShapesBar;
|
||||||
|
rt;
|
||||||
|
wing;
|
||||||
|
width;
|
||||||
|
height;
|
||||||
|
return generateBorderEdgePath(width, height);
|
||||||
|
}
|
||||||
property bool mainPathCorrectShape: false
|
property bool mainPathCorrectShape: false
|
||||||
property bool borderFullPathCorrectShape: false
|
property bool borderFullPathCorrectShape: false
|
||||||
property bool borderEdgePathCorrectShape: false
|
property bool borderEdgePathCorrectShape: false
|
||||||
@@ -136,6 +161,12 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFrameShapesBarChanged: {
|
||||||
|
mainPathCorrectShape = false;
|
||||||
|
borderFullPathCorrectShape = false;
|
||||||
|
borderEdgePathCorrectShape = false;
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
@@ -259,7 +290,7 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
const crE = frameShapesBar ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${crE} 0`;
|
let d = `M ${crE} 0`;
|
||||||
d += ` L ${w - crE} 0`;
|
d += ` L ${w - crE} 0`;
|
||||||
@@ -290,7 +321,7 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
const crE = frameShapesBar ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${crE} ${fullH}`;
|
let d = `M ${crE} ${fullH}`;
|
||||||
d += ` L ${w - crE} ${fullH}`;
|
d += ` L ${w - crE} ${fullH}`;
|
||||||
@@ -320,7 +351,7 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
const crE = frameShapesBar ? 0 : cr;
|
||||||
|
|
||||||
let d = `M 0 ${crE}`;
|
let d = `M 0 ${crE}`;
|
||||||
d += ` L 0 ${h - crE}`;
|
d += ` L 0 ${h - crE}`;
|
||||||
@@ -351,7 +382,7 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
const crE = frameShapesBar ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${fullW} ${crE}`;
|
let d = `M ${fullW} ${crE}`;
|
||||||
d += ` L ${fullW} ${h - crE}`;
|
d += ` L ${fullW} ${h - crE}`;
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ Item {
|
|||||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||||
readonly property real _edgeBaseMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
readonly property real _edgeBaseMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||||
readonly property real _frameEdgeFloorInset: SettingsData.frameEnabled ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
|
|
||||||
readonly property bool _hasBarWindow: barWindow !== undefined && barWindow !== null
|
readonly property bool _hasBarWindow: barWindow !== undefined && barWindow !== null
|
||||||
|
readonly property bool _usesFrameBarChrome: _hasBarWindow && (barWindow.usesFrameBarChrome ?? false)
|
||||||
|
readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
|
||||||
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
|
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
|
||||||
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
|
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
|
||||||
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
|
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
|
||||||
@@ -47,22 +48,22 @@ Item {
|
|||||||
_hadAdjacentRightBar = true
|
_hadAdjacentRightBar = true
|
||||||
|
|
||||||
readonly property real _frameLeftInset: {
|
readonly property real _frameLeftInset: {
|
||||||
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical)
|
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
|
||||||
return 0;
|
return 0;
|
||||||
return hasAdjacentLeftBarLive ? SettingsData.frameBarSize : (_hadAdjacentLeftBar ? _frameEdgeFloorInset : 0);
|
return hasAdjacentLeftBarLive ? SettingsData.frameBarSize : (_hadAdjacentLeftBar ? _frameEdgeFloorInset : 0);
|
||||||
}
|
}
|
||||||
readonly property real _frameRightInset: {
|
readonly property real _frameRightInset: {
|
||||||
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical)
|
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
|
||||||
return 0;
|
return 0;
|
||||||
return hasAdjacentRightBarLive ? SettingsData.frameBarSize : (_hadAdjacentRightBar ? _frameEdgeFloorInset : 0);
|
return hasAdjacentRightBarLive ? SettingsData.frameBarSize : (_hadAdjacentRightBar ? _frameEdgeFloorInset : 0);
|
||||||
}
|
}
|
||||||
readonly property real _frameTopInset: {
|
readonly property real _frameTopInset: {
|
||||||
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical)
|
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
|
||||||
return 0;
|
return 0;
|
||||||
return hasAdjacentTopBarLive ? SettingsData.frameThickness : (_hadAdjacentTopBar ? _frameEdgeFloorInset : 0);
|
return hasAdjacentTopBarLive ? SettingsData.frameThickness : (_hadAdjacentTopBar ? _frameEdgeFloorInset : 0);
|
||||||
}
|
}
|
||||||
readonly property real _frameBottomInset: {
|
readonly property real _frameBottomInset: {
|
||||||
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical)
|
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
|
||||||
return 0;
|
return 0;
|
||||||
return hasAdjacentBottomBarLive ? SettingsData.frameThickness : (_hadAdjacentBottomBar ? _frameEdgeFloorInset : 0);
|
return hasAdjacentBottomBarLive ? SettingsData.frameThickness : (_hadAdjacentBottomBar ? _frameEdgeFloorInset : 0);
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on anchors.leftMargin {
|
Behavior on anchors.leftMargin {
|
||||||
enabled: _animateFrameInsets && SettingsData.frameEnabled
|
enabled: _animateFrameInsets && _usesFrameBarChrome
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
@@ -103,7 +104,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on anchors.rightMargin {
|
Behavior on anchors.rightMargin {
|
||||||
enabled: _animateFrameInsets && SettingsData.frameEnabled
|
enabled: _animateFrameInsets && _usesFrameBarChrome
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
@@ -111,7 +112,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on anchors.topMargin {
|
Behavior on anchors.topMargin {
|
||||||
enabled: _animateFrameInsets && SettingsData.frameEnabled
|
enabled: _animateFrameInsets && _usesFrameBarChrome
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
@@ -119,7 +120,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on anchors.bottomMargin {
|
Behavior on anchors.bottomMargin {
|
||||||
enabled: _animateFrameInsets && SettingsData.frameEnabled
|
enabled: _animateFrameInsets && _usesFrameBarChrome
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: Theme.shortDuration
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ PanelWindow {
|
|||||||
triggerDashTab(2);
|
triggerDashTab(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false)
|
||||||
|
|
||||||
readonly property var dBarLayer: {
|
readonly property var dBarLayer: {
|
||||||
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
|
||||||
case "bottom":
|
case "bottom":
|
||||||
@@ -119,10 +121,7 @@ PanelWindow {
|
|||||||
case "top":
|
case "top":
|
||||||
return WlrLayer.Top;
|
return WlrLayer.Top;
|
||||||
default:
|
default:
|
||||||
// Elevate to Overlay when Frame is enabled so the bar stays above
|
return barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top;
|
||||||
// the FrameWindow (WlrLayer.Top) when it is re-mapped on mode switch,
|
|
||||||
// but drop back to Top while a true fullscreen app owns this screen.
|
|
||||||
return SettingsData.frameEnabled && !barWindow.hasFullscreenToplevel ? WlrLayer.Overlay : WlrLayer.Top;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +151,16 @@ PanelWindow {
|
|||||||
onTriggered: barBlur.rebuild()
|
onTriggered: barBlur.rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: barWindow
|
||||||
|
function onUsesConnectedFrameChromeChanged() {
|
||||||
|
_blurRebuildTimer.restart();
|
||||||
|
}
|
||||||
|
function onUsesFrameBarChromeChanged() {
|
||||||
|
_blurRebuildTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: blurRegionComp
|
id: blurRegionComp
|
||||||
Region {}
|
Region {}
|
||||||
@@ -179,7 +188,7 @@ PanelWindow {
|
|||||||
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
||||||
// (including the bar area). The bar must not set its own competing blur region
|
// (including the bar area). The bar must not set its own competing blur region
|
||||||
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
||||||
if (SettingsData.frameEnabled)
|
if (SettingsData.frameEnabled && barWindow.usesFrameBarChrome)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||||
@@ -292,7 +301,7 @@ PanelWindow {
|
|||||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||||
readonly property string _barId: barConfig?.id ?? "default"
|
readonly property string _barId: barConfig?.id ?? "default"
|
||||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||||
readonly property color _bgColor: SettingsData.frameEnabled ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
readonly property color _bgColor: (SettingsData.frameEnabled && usesFrameBarChrome) ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||||
|
|
||||||
function _updateBackgroundAlpha() {
|
function _updateBackgroundAlpha() {
|
||||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||||
@@ -316,16 +325,14 @@ PanelWindow {
|
|||||||
|
|
||||||
property string screenName: modelData.name
|
property string screenName: modelData.name
|
||||||
|
|
||||||
|
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
|
||||||
|
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
|
||||||
|
|
||||||
|
// Flatten/spacing collapse for maximized windows is only for frame-integrated layout.
|
||||||
|
// When the bar draws its own pill, keep rounded corners and spacing like the dock.
|
||||||
|
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
|
||||||
|
|
||||||
property bool hasMaximizedToplevel: false
|
property bool hasMaximizedToplevel: false
|
||||||
readonly property bool hasFullscreenToplevel: {
|
|
||||||
if (!(barConfig?.fullscreenDetection ?? true))
|
|
||||||
return false;
|
|
||||||
CompositorService.sortedToplevels;
|
|
||||||
ToplevelManager.activeToplevel;
|
|
||||||
if (CompositorService.isNiri)
|
|
||||||
NiriService.allWorkspaces;
|
|
||||||
return CompositorService.hasFullscreenToplevelOnScreen(screenName);
|
|
||||||
}
|
|
||||||
property bool shouldHideForWindows: false
|
property bool shouldHideForWindows: false
|
||||||
|
|
||||||
function _updateHasMaximizedToplevel() {
|
function _updateHasMaximizedToplevel() {
|
||||||
@@ -427,7 +434,7 @@ PanelWindow {
|
|||||||
shouldHideForWindows = filtered.length > 0;
|
shouldHideForWindows = filtered.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
|
property real effectiveSpacing: (SettingsData.frameEnabled && usesFrameBarChrome) ? 0 : ((flattenForMaximizedWindow && hasMaximizedToplevel) ? 0 : (barConfig?.spacing ?? 4))
|
||||||
|
|
||||||
Behavior on effectiveSpacing {
|
Behavior on effectiveSpacing {
|
||||||
enabled: barWindow.visible
|
enabled: barWindow.visible
|
||||||
@@ -438,7 +445,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property int notificationCount: NotificationService.notifications.length
|
readonly property int notificationCount: NotificationService.notifications.length
|
||||||
readonly property real effectiveBarThickness: SettingsData.frameEnabled ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
readonly property real effectiveBarThickness: (SettingsData.frameEnabled && usesFrameBarChrome) ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||||
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false)
|
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled ? SettingsData.frameShowOnOverview : (barConfig?.openOnOverview ?? false)
|
||||||
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||||
|
|
||||||
@@ -636,9 +643,9 @@ PanelWindow {
|
|||||||
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
|
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
|
||||||
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
|
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
|
||||||
|
|
||||||
readonly property bool reserveExclusiveWhenAutoHidden: SettingsData.connectedFrameModeActive && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences)
|
readonly property bool reserveExclusiveWhenAutoHidden: SettingsData.frameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences)
|
||||||
|
|
||||||
exclusiveZone: (barWindow.hasFullscreenToplevel || !(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0)))
|
exclusiveZone: (!(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0)))
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: inputMask
|
id: inputMask
|
||||||
@@ -647,9 +654,9 @@ PanelWindow {
|
|||||||
|
|
||||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||||
readonly property bool showing: effectiveVisible && !barWindow.hasFullscreenToplevel && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||||
|
|
||||||
readonly property int maskThickness: barWindow.hasFullscreenToplevel ? 0 : (showing ? barThickness : 1)
|
readonly property int maskThickness: showing ? barThickness : 1
|
||||||
|
|
||||||
x: {
|
x: {
|
||||||
if (!axis.isVertical) {
|
if (!axis.isVertical) {
|
||||||
@@ -719,7 +726,7 @@ PanelWindow {
|
|||||||
item: clickThroughEnabled ? null : inputMask
|
item: clickThroughEnabled ? null : inputMask
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -732,7 +739,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -745,7 +752,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress + barWindow.width * 0) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -826,9 +833,6 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool reveal: {
|
property bool reveal: {
|
||||||
if (barWindow.hasFullscreenToplevel)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||||
if (inOverviewWithShow)
|
if (inOverviewWithShow)
|
||||||
return true;
|
return true;
|
||||||
@@ -897,9 +901,9 @@ PanelWindow {
|
|||||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||||
}
|
}
|
||||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !barWindow.hasFullscreenToplevel && !topBarCore.popoutPinsReveal
|
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
enabled: (barConfig?.autoHide ?? false) && !inOverview && !barWindow.hasFullscreenToplevel
|
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: topBarContainer
|
id: topBarContainer
|
||||||
|
|||||||
@@ -131,9 +131,19 @@ BasePill {
|
|||||||
function getNetworkIconColor() {
|
function getNetworkIconColor() {
|
||||||
if (NetworkService.wifiToggling)
|
if (NetworkService.wifiToggling)
|
||||||
return Theme.primary;
|
return Theme.primary;
|
||||||
|
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
|
||||||
|
return Theme.primary;
|
||||||
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
|
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIconBlinking(id) {
|
||||||
|
if (id === "network")
|
||||||
|
return NetworkService.isWifiConnecting;
|
||||||
|
if (id === "bluetooth")
|
||||||
|
return BluetoothService.connecting;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function getVolumeIconName() {
|
function getVolumeIconName() {
|
||||||
if (!AudioService.sink?.audio)
|
if (!AudioService.sink?.audio)
|
||||||
return "volume_up";
|
return "volume_up";
|
||||||
@@ -485,6 +495,7 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
|
id: vIconOnlyItem
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: !verticalGroupItem.modelData.composite
|
visible: !verticalGroupItem.modelData.composite
|
||||||
name: {
|
name: {
|
||||||
@@ -515,7 +526,7 @@ BasePill {
|
|||||||
case "vpn":
|
case "vpn":
|
||||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
|
||||||
case "battery":
|
case "battery":
|
||||||
return root.getBatteryIconColor();
|
return root.getBatteryIconColor();
|
||||||
case "printer":
|
case "printer":
|
||||||
@@ -524,6 +535,11 @@ BasePill {
|
|||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: vIconOnlyItem
|
||||||
|
running: root.getIconBlinking(verticalGroupItem.modelData.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
@@ -687,7 +703,7 @@ BasePill {
|
|||||||
case "vpn":
|
case "vpn":
|
||||||
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
|
||||||
case "bluetooth":
|
case "bluetooth":
|
||||||
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
|
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
|
||||||
case "battery":
|
case "battery":
|
||||||
return root.getBatteryIconColor();
|
return root.getBatteryIconColor();
|
||||||
case "printer":
|
case "printer":
|
||||||
@@ -696,6 +712,11 @@ BasePill {
|
|||||||
return Theme.widgetIconColor;
|
return Theme.widgetIconColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankBlink {
|
||||||
|
target: iconOnlyItem
|
||||||
|
running: root.getIconBlinking(horizontalGroupItem.modelData.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
@@ -14,9 +15,20 @@ BasePill {
|
|||||||
|
|
||||||
property var widgetData: null
|
property var widgetData: null
|
||||||
property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode
|
property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode
|
||||||
property int availableWidth: 400
|
readonly property int maxWidth: {
|
||||||
readonly property int maxNormalWidth: 456
|
const size = widgetData?.focusedWindowSize !== undefined ? widgetData.focusedWindowSize : SettingsData.focusedWindowSize;
|
||||||
readonly property int maxCompactWidth: 288
|
switch (size) {
|
||||||
|
case 0:
|
||||||
|
return 288;
|
||||||
|
case 2:
|
||||||
|
return 656;
|
||||||
|
case 3:
|
||||||
|
return 856;
|
||||||
|
default:
|
||||||
|
return 456;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
property int availableWidth: maxWidth
|
||||||
property Toplevel activeWindow: null
|
property Toplevel activeWindow: null
|
||||||
property var activeDesktopEntry: null
|
property var activeDesktopEntry: null
|
||||||
property bool isHovered: mouseArea.containsMouse
|
property bool isHovered: mouseArea.containsMouse
|
||||||
@@ -171,8 +183,7 @@ BasePill {
|
|||||||
return 0;
|
return 0;
|
||||||
if (root.isVerticalOrientation)
|
if (root.isVerticalOrientation)
|
||||||
return root.widgetThickness - root.horizontalPadding * 2;
|
return root.widgetThickness - root.horizontalPadding * 2;
|
||||||
const baseWidth = contentRow.implicitWidth;
|
return contentRow.implicitWidth;
|
||||||
return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2);
|
|
||||||
}
|
}
|
||||||
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
|
||||||
clip: false
|
clip: false
|
||||||
@@ -222,7 +233,7 @@ BasePill {
|
|||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
id: contentRow
|
id: contentRow
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
@@ -231,24 +242,23 @@ BasePill {
|
|||||||
StyledText {
|
StyledText {
|
||||||
id: appText
|
id: appText
|
||||||
text: {
|
text: {
|
||||||
if (!activeWindow || !activeWindow.appId)
|
if (compactMode || !activeWindow || !activeWindow.appId)
|
||||||
return "";
|
return "";
|
||||||
return Paths.getAppName(activeWindow.appId, activeDesktopEntry);
|
return Paths.getAppName(activeWindow.appId, activeDesktopEntry);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
width: Math.min(implicitWidth, compactMode ? 80 : 180)
|
Layout.maximumWidth: compactMode ? 80 : 180
|
||||||
visible: !compactMode && text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: "•"
|
id: appSeparator
|
||||||
|
text: compactMode ? "" : "•"
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.outlineButton
|
color: Theme.outlineButton
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !compactMode && appText.text && titleText.text
|
visible: !compactMode && appText.text && titleText.text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,10 +286,9 @@ BasePill {
|
|||||||
}
|
}
|
||||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
width: Math.min(implicitWidth, compactMode ? 280 : 250)
|
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
|
||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ BasePill {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
|
readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
|
||||||
|
readonly property string targetScreenName: parentScreen?.name || focusedScreenName
|
||||||
|
|
||||||
function resolveNotepadInstance() {
|
function resolveNotepadInstance() {
|
||||||
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
|
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetScreen = focusedScreenName;
|
const targetScreen = targetScreenName;
|
||||||
if (targetScreen) {
|
if (targetScreen) {
|
||||||
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
|
||||||
var slideout = notepadSlideoutVariants.instances[i];
|
var slideout = notepadSlideoutVariants.instances[i];
|
||||||
@@ -34,6 +35,12 @@ BasePill {
|
|||||||
readonly property bool isActive: notepadInstance?.isVisible ?? false
|
readonly property bool isActive: notepadInstance?.isVisible ?? false
|
||||||
property bool isAutoHideBar: false
|
property bool isAutoHideBar: false
|
||||||
|
|
||||||
|
function prepareNotepadInstance(instance) {
|
||||||
|
if (instance)
|
||||||
|
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property real minTooltipY: {
|
readonly property real minTooltipY: {
|
||||||
if (!parentScreen || !(axis?.isVertical ?? false)) {
|
if (!parentScreen || !(axis?.isVertical ?? false)) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -68,8 +75,9 @@ BasePill {
|
|||||||
function openTabByIndex(tabIndex) {
|
function openTabByIndex(tabIndex) {
|
||||||
if (tabIndex < 0)
|
if (tabIndex < 0)
|
||||||
return;
|
return;
|
||||||
if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
|
const instance = prepareNotepadInstance(root.notepadInstance);
|
||||||
root.notepadInstance.show();
|
if (instance && typeof instance.show === "function") {
|
||||||
|
instance.show();
|
||||||
}
|
}
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
NotepadStorageService.switchToTab(tabIndex);
|
NotepadStorageService.switchToTab(tabIndex);
|
||||||
@@ -77,8 +85,9 @@ BasePill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openNewNote() {
|
function openNewNote() {
|
||||||
if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
|
const instance = prepareNotepadInstance(root.notepadInstance);
|
||||||
root.notepadInstance.show();
|
if (instance && typeof instance.show === "function") {
|
||||||
|
instance.show();
|
||||||
}
|
}
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
NotepadStorageService.createNewTab();
|
NotepadStorageService.createNewTab();
|
||||||
@@ -138,7 +147,7 @@ BasePill {
|
|||||||
openContextMenu();
|
openContextMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const inst = root.notepadInstance;
|
const inst = prepareNotepadInstance(root.notepadInstance);
|
||||||
if (inst) {
|
if (inst) {
|
||||||
inst.toggle();
|
inst.toggle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -978,7 +978,7 @@ BasePill {
|
|||||||
|
|
||||||
visible: root.useOverflowPopup && root.menuOpen
|
visible: root.useOverflowPopup && root.menuOpen
|
||||||
screen: root.parentScreen
|
screen: root.parentScreen
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: {
|
WlrLayershell.keyboardFocus: {
|
||||||
if (!root.menuOpen)
|
if (!root.menuOpen)
|
||||||
@@ -1446,7 +1446,7 @@ BasePill {
|
|||||||
WlrLayershell.namespace: "dms:tray-menu-window"
|
WlrLayershell.namespace: "dms:tray-menu-window"
|
||||||
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
|
||||||
screen: menuRoot.parentScreen
|
screen: menuRoot.parentScreen
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: {
|
WlrLayershell.keyboardFocus: {
|
||||||
if (!menuRoot.showMenu)
|
if (!menuRoot.showMenu)
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ Variants {
|
|||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
targetWindow: dock
|
targetWindow: dock
|
||||||
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
|
blurEnabled: dock.effectiveBlurEnabled && !dock.usesConnectedFrameChrome
|
||||||
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||||
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||||
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
|
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
|
||||||
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
|
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
|
||||||
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
|
blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:dock"
|
WlrLayershell.namespace: "dms:dock"
|
||||||
WlrLayershell.layer: SettingsData.frameEnabled && !dock.hasFullscreenToplevel ? WlrLayer.Overlay : WlrLayer.Top
|
WlrLayershell.layer: dock.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
|
||||||
|
|
||||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||||
|
|
||||||
@@ -50,16 +50,16 @@ Variants {
|
|||||||
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
|
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
|
||||||
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
|
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
|
||||||
readonly property real dockFrameInset: dockGeometry.frameInset
|
readonly property real dockFrameInset: dockGeometry.frameInset
|
||||||
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
|
readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||||
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||||
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
|
readonly property color surfaceBorderColor: usesConnectedFrameChrome ? "transparent" : BlurService.borderColor
|
||||||
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
|
readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth
|
||||||
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||||
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||||
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||||
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||||
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
|
readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0
|
||||||
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
|
readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0
|
||||||
|
|
||||||
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
|
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
|
||||||
|
|
||||||
@@ -149,7 +149,6 @@ Variants {
|
|||||||
edge: dock.connectedBarSide
|
edge: dock.connectedBarSide
|
||||||
dockVisible: dock.visible
|
dockVisible: dock.visible
|
||||||
autoHide: dock.autoHide
|
autoHide: dock.autoHide
|
||||||
hasFullscreenToplevel: dock.hasFullscreenToplevel
|
|
||||||
iconSize: dock.widgetHeight
|
iconSize: dock.widgetHeight
|
||||||
spacing: SettingsData.dockSpacing
|
spacing: SettingsData.dockSpacing
|
||||||
borderThickness: dock.borderThickness
|
borderThickness: dock.borderThickness
|
||||||
@@ -176,25 +175,13 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
|
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
|
||||||
readonly property bool hasFullscreenToplevel: {
|
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName)
|
||||||
if (!SettingsData.dockHideOnFullscreen)
|
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer
|
||||||
return false;
|
|
||||||
CompositorService.sortedToplevels;
|
|
||||||
ToplevelManager.activeToplevel;
|
|
||||||
if (CompositorService.isNiri) {
|
|
||||||
NiriService.currentOutput;
|
|
||||||
NiriService.windows;
|
|
||||||
NiriService.allWorkspaces;
|
|
||||||
}
|
|
||||||
if (CompositorService.isHyprland)
|
|
||||||
Hyprland.focusedWorkspace;
|
|
||||||
return CompositorService.hasFullscreenToplevelOnScreen(dock._dockScreenName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _syncDockChromeState() {
|
function _syncDockChromeState() {
|
||||||
if (!dock._dockScreenName)
|
if (!dock._dockScreenName)
|
||||||
return;
|
return;
|
||||||
if (!SettingsData.connectedFrameModeActive) {
|
if (!dock.usesConnectedFrameChrome) {
|
||||||
ConnectedModeState.clearDockState(dock._dockScreenName);
|
ConnectedModeState.clearDockState(dock._dockScreenName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -212,19 +199,19 @@ Variants {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _syncDockSlide() {
|
function _syncDockSlide() {
|
||||||
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
|
if (!dock._dockScreenName || !dock.usesConnectedFrameChrome)
|
||||||
return;
|
return;
|
||||||
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
|
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeferredAction {
|
DeferredAction {
|
||||||
id: dockSlideSync
|
id: dockSlideSync
|
||||||
enabled: SettingsData.connectedFrameModeActive
|
enabled: dock.usesConnectedFrameChrome
|
||||||
onTriggered: dock._syncDockSlide()
|
onTriggered: dock._syncDockSlide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function _queueSlideSync() {
|
function _queueSlideSync() {
|
||||||
if (!SettingsData.connectedFrameModeActive)
|
if (!dock.usesConnectedFrameChrome)
|
||||||
return;
|
return;
|
||||||
dockSlideSync.schedule();
|
dockSlideSync.schedule();
|
||||||
}
|
}
|
||||||
@@ -304,65 +291,10 @@ Variants {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hyprland implementation
|
// Hyprland implementation (current workspace + visible special workspaces)
|
||||||
Hyprland.focusedWorkspace;
|
Hyprland.focusedWorkspace;
|
||||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
Hyprland.toplevels;
|
||||||
|
return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
|
||||||
if (filtered.length === 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (let i = 0; i < filtered.length; i++) {
|
|
||||||
const toplevel = filtered[i];
|
|
||||||
|
|
||||||
let hyprToplevel = null;
|
|
||||||
if (Hyprland.toplevels) {
|
|
||||||
const hyprToplevels = Array.from(Hyprland.toplevels.values);
|
|
||||||
for (let j = 0; j < hyprToplevels.length; j++) {
|
|
||||||
if (hyprToplevels[j].wayland === toplevel) {
|
|
||||||
hyprToplevel = hyprToplevels[j];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hyprToplevel?.lastIpcObject)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const ipc = hyprToplevel.lastIpcObject;
|
|
||||||
const at = ipc.at;
|
|
||||||
const size = ipc.size;
|
|
||||||
if (!at || !size)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const monX = hyprToplevel.monitor?.x ?? 0;
|
|
||||||
const monY = hyprToplevel.monitor?.y ?? 0;
|
|
||||||
|
|
||||||
const winX = at[0] - monX;
|
|
||||||
const winY = at[1] - monY;
|
|
||||||
const winW = size[0];
|
|
||||||
const winH = size[1];
|
|
||||||
|
|
||||||
switch (SettingsData.dockPosition) {
|
|
||||||
case SettingsData.Position.Top:
|
|
||||||
if (winY < dockThickness)
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
case SettingsData.Position.Bottom:
|
|
||||||
if (winY + winH > screenHeight - dockThickness)
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
case SettingsData.Position.Left:
|
|
||||||
if (winX < dockThickness)
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
case SettingsData.Position.Right:
|
|
||||||
if (winX + winW > screenWidth - dockThickness)
|
|
||||||
return true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -383,9 +315,6 @@ Variants {
|
|||||||
if (_modalRetractActive)
|
if (_modalRetractActive)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (dock.hasFullscreenToplevel)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -421,7 +350,7 @@ Variants {
|
|||||||
onVisibleChanged: dock._syncDockChromeState()
|
onVisibleChanged: dock._syncDockChromeState()
|
||||||
onHasAppsChanged: dock._syncDockChromeState()
|
onHasAppsChanged: dock._syncDockChromeState()
|
||||||
onConnectedBarSideChanged: dock._syncDockChromeState()
|
onConnectedBarSideChanged: dock._syncDockChromeState()
|
||||||
onHasFullscreenToplevelChanged: dock._syncDockChromeState()
|
onUsesConnectedFrameChromeChanged: dock._syncDockChromeState()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
@@ -680,7 +609,7 @@ Variants {
|
|||||||
return 0;
|
return 0;
|
||||||
if (dock.reveal)
|
if (dock.reveal)
|
||||||
return 0;
|
return 0;
|
||||||
if (Theme.isConnectedEffect) {
|
if (dock.usesConnectedFrameChrome) {
|
||||||
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
|
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
|
||||||
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
|
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
|
||||||
}
|
}
|
||||||
@@ -696,7 +625,7 @@ Variants {
|
|||||||
return 0;
|
return 0;
|
||||||
if (dock.reveal)
|
if (dock.reveal)
|
||||||
return 0;
|
return 0;
|
||||||
if (Theme.isConnectedEffect) {
|
if (dock.usesConnectedFrameChrome) {
|
||||||
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
|
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
|
||||||
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
|
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
|
||||||
}
|
}
|
||||||
@@ -711,9 +640,9 @@ Variants {
|
|||||||
Behavior on x {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: slideXAnimation
|
id: slideXAnimation
|
||||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
|
||||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||||
onRunningChanged: if (!running)
|
onRunningChanged: if (!running)
|
||||||
dock._syncDockChromeState()
|
dock._syncDockChromeState()
|
||||||
}
|
}
|
||||||
@@ -722,9 +651,9 @@ Variants {
|
|||||||
Behavior on y {
|
Behavior on y {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: slideYAnimation
|
id: slideYAnimation
|
||||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
|
||||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||||
onRunningChanged: if (!running)
|
onRunningChanged: if (!running)
|
||||||
dock._syncDockChromeState()
|
dock._syncDockChromeState()
|
||||||
}
|
}
|
||||||
@@ -756,12 +685,12 @@ Variants {
|
|||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
|
|
||||||
// Avoid an offscreen texture seam where the connected dock meets the frame.
|
// Avoid an offscreen texture seam where the connected dock meets the frame.
|
||||||
layer.enabled: !Theme.isConnectedEffect
|
layer.enabled: !usesConnectedFrameChrome
|
||||||
clip: false
|
clip: false
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
|
visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
|
||||||
color: dock.surfaceColor
|
color: dock.surfaceColor
|
||||||
topLeftRadius: dock.surfaceTopLeftRadius
|
topLeftRadius: dock.surfaceTopLeftRadius
|
||||||
topRightRadius: dock.surfaceTopRightRadius
|
topRightRadius: dock.surfaceTopRightRadius
|
||||||
@@ -771,7 +700,7 @@ Variants {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
|
visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
topLeftRadius: dock.surfaceTopLeftRadius
|
topLeftRadius: dock.surfaceTopLeftRadius
|
||||||
topRightRadius: dock.surfaceTopRightRadius
|
topRightRadius: dock.surfaceTopRightRadius
|
||||||
@@ -807,7 +736,7 @@ Variants {
|
|||||||
y: dockBackground.y - borderThickness
|
y: dockBackground.y - borderThickness
|
||||||
width: dockBackground.width + borderThickness * 2
|
width: dockBackground.width + borderThickness * 2
|
||||||
height: dockBackground.height + borderThickness * 2
|
height: dockBackground.height + borderThickness * 2
|
||||||
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
|
visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
readonly property real borderThickness: Math.max(1, dock.borderThickness)
|
readonly property real borderThickness: Math.max(1, dock.borderThickness)
|
||||||
@@ -883,6 +812,7 @@ Variants {
|
|||||||
isVertical: dock.isVertical
|
isVertical: dock.isVertical
|
||||||
dockScreen: dock.screen
|
dockScreen: dock.screen
|
||||||
iconSize: dock.widgetHeight
|
iconSize: dock.widgetHeight
|
||||||
|
usesOverlayLayer: dock.usesOverlayLayer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Item {
|
|||||||
property bool isVertical: false
|
property bool isVertical: false
|
||||||
property var dockScreen: null
|
property var dockScreen: null
|
||||||
property real iconSize: 40
|
property real iconSize: 40
|
||||||
|
property bool usesOverlayLayer: false
|
||||||
property int draggedIndex: -1
|
property int draggedIndex: -1
|
||||||
property int dropTargetIndex: -1
|
property int dropTargetIndex: -1
|
||||||
property bool suppressShiftAnimation: false
|
property bool suppressShiftAnimation: false
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: root
|
id: root
|
||||||
@@ -10,7 +11,6 @@ QtObject {
|
|||||||
property string edge: "bottom"
|
property string edge: "bottom"
|
||||||
property bool dockVisible: false
|
property bool dockVisible: false
|
||||||
property bool autoHide: false
|
property bool autoHide: false
|
||||||
property bool hasFullscreenToplevel: false
|
|
||||||
property real iconSize: 40
|
property real iconSize: 40
|
||||||
property real spacing: 4
|
property real spacing: 4
|
||||||
property real borderThickness: 0
|
property real borderThickness: 0
|
||||||
@@ -23,14 +23,14 @@ QtObject {
|
|||||||
return Math.round(value * dpr) / dpr;
|
return Math.round(value * dpr) / dpr;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool frameExclusionActive: SettingsData.frameEnabled && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences)
|
readonly property bool frameExclusionActive: CompositorService.frameWindowVisibleForScreen(screen)
|
||||||
readonly property bool connectedMode: Theme.isConnectedEffect
|
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screen)
|
||||||
readonly property bool connectedBarActiveOnEdge: connectedMode && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
|
readonly property bool connectedBarActiveOnEdge: usesConnectedFrameChrome && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
|
||||||
|
|
||||||
readonly property real connectedJoinInset: {
|
readonly property real connectedJoinInset: {
|
||||||
if (connectedMode)
|
if (usesConnectedFrameChrome)
|
||||||
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||||
if (SettingsData.frameEnabled)
|
if (frameExclusionActive)
|
||||||
return SettingsData.frameEdgeInsetForSide(screen, edge);
|
return SettingsData.frameEdgeInsetForSide(screen, edge);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -38,15 +38,15 @@ QtObject {
|
|||||||
readonly property real frameInset: {
|
readonly property real frameInset: {
|
||||||
if (!frameExclusionActive)
|
if (!frameExclusionActive)
|
||||||
return 0;
|
return 0;
|
||||||
if (connectedMode)
|
if (usesConnectedFrameChrome)
|
||||||
return connectedJoinInset;
|
return connectedJoinInset;
|
||||||
return SettingsData.frameThickness;
|
return SettingsData.frameThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real effectiveMargin: connectedMode ? 0 : margin
|
readonly property real effectiveMargin: usesConnectedFrameChrome ? 0 : margin
|
||||||
readonly property real visualOffset: connectedMode ? 0 : offset
|
readonly property real visualOffset: usesConnectedFrameChrome ? 0 : offset
|
||||||
readonly property real reserveOffset: offset
|
readonly property real reserveOffset: offset
|
||||||
readonly property real joinedEdgeMargin: connectedMode ? 0 : (barSpacing + effectiveMargin + 1 + borderThickness)
|
readonly property real joinedEdgeMargin: usesConnectedFrameChrome ? 0 : (barSpacing + effectiveMargin + 1 + borderThickness)
|
||||||
readonly property real bodyEdgeMargin: frameInset + joinedEdgeMargin
|
readonly property real bodyEdgeMargin: frameInset + joinedEdgeMargin
|
||||||
|
|
||||||
readonly property real bodyThickness: iconSize + spacing * 2 + borderThickness * 2
|
readonly property real bodyThickness: iconSize + spacing * 2 + borderThickness * 2
|
||||||
@@ -57,5 +57,5 @@ QtObject {
|
|||||||
// Frame/bar edge exclusions already reserve the edge itself, so the dock
|
// Frame/bar edge exclusions already reserve the edge itself, so the dock
|
||||||
// reservation covers only the dock body and user offset beyond that edge.
|
// reservation covers only the dock body and user offset beyond that edge.
|
||||||
readonly property real reserveZone: px(bodyThickness + reserveOffset + effectiveMargin)
|
readonly property real reserveZone: px(bodyThickness + reserveOffset + effectiveMargin)
|
||||||
readonly property bool shouldReserveSpace: dockVisible && !hasFullscreenToplevel && !autoHide && barSpacing <= 0
|
readonly property bool shouldReserveSpace: dockVisible && !autoHide && barSpacing <= 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ Item {
|
|||||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PopoutService.toggleDankLauncherV2();
|
PopoutService.toggleDankLauncherV2(dockApps?.usesOverlayLayer ?? false);
|
||||||
}
|
}
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (longPressing && !dragging) {
|
if (longPressing && !dragging) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
id: root
|
id: root
|
||||||
@@ -18,7 +19,7 @@ Scope {
|
|||||||
// One thin invisible PanelWindow per edge.
|
// One thin invisible PanelWindow per edge.
|
||||||
// Skips any edge where a bar already provides its own exclusiveZone.
|
// Skips any edge where a bar already provides its own exclusiveZone.
|
||||||
|
|
||||||
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
|
readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen)
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: root.screenEnabled && !root.barEdges.includes("top")
|
active: root.screenEnabled && !root.barEdges.includes("top")
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ PanelWindow {
|
|||||||
required property var targetScreen
|
required property var targetScreen
|
||||||
|
|
||||||
screen: targetScreen
|
screen: targetScreen
|
||||||
visible: _frameActive
|
readonly property bool _frameVisible: CompositorService.frameWindowVisibleForScreen(win.targetScreen)
|
||||||
updatesEnabled: _frameActive
|
visible: win._frameVisible
|
||||||
|
updatesEnabled: win._frameVisible
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:frame"
|
WlrLayershell.namespace: "dms:frame"
|
||||||
WlrLayershell.layer: WlrLayer.Top
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
@@ -52,7 +53,7 @@ PanelWindow {
|
|||||||
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
|
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
|
||||||
readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState
|
readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState
|
||||||
|
|
||||||
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive
|
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
|
||||||
readonly property string _barSide: {
|
readonly property string _barSide: {
|
||||||
const edges = win.barEdges;
|
const edges = win.barEdges;
|
||||||
if (edges.includes("top"))
|
if (edges.includes("top"))
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ sudo rpm -ivh x86_64/dms-greeter-*.rpm
|
|||||||
```
|
```
|
||||||
|
|
||||||
The package automatically:
|
The package automatically:
|
||||||
- Creates the greeter user
|
|
||||||
|
- Creates the greeter user (via `systemd-sysusers` from `/usr/lib/sysusers.d/dms-greeter.conf` for atomic/immutable compatibility, with package script fallback)
|
||||||
- Sets up directories and permissions
|
- Sets up directories and permissions
|
||||||
- Configures greetd with auto-detected compositor
|
- Configures greetd with auto-detected compositor
|
||||||
- Applies SELinux contexts
|
- Applies SELinux contexts
|
||||||
@@ -178,7 +179,7 @@ sudo systemctl enable greetd
|
|||||||
#### Legacy installation (deprecated)
|
#### Legacy installation (deprecated)
|
||||||
|
|
||||||
If you prefer the old method with separate shell scripts and config files:
|
If you prefer the old method with separate shell scripts and config files:
|
||||||
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.conf` to `/etc/greetd`
|
1. Copy `assets/dms-niri.kdl` or `assets/dms-hypr.lua` (legacy: `assets/dms-hypr.conf`) to `/etc/greetd`
|
||||||
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
2. Copy `assets/greet-niri.sh` or `assets/greet-hyprland.sh` to `/usr/local/bin/start-dms-greetd.sh`
|
||||||
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
|
3. Edit the config file and replace `_DMS_PATH_` with your DMS installation path
|
||||||
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
|
4. Configure greetd to use `/usr/local/bin/start-dms-greetd.sh`
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Deprecated: greetd expects Hyprland 0.55+ Lua; use `/etc/greetd/dms-hypr.lua` instead.
|
||||||
env = DMS_RUN_GREETER,1
|
env = DMS_RUN_GREETER,1
|
||||||
|
|
||||||
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
exec = sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- Minimal Hyprland (Lua) session for greetd — replace _DMS_PATH_ with your DMS checkout.
|
||||||
|
-- Copy to `/etc/greetd/dms-hypr.lua` alongside `greet-hyprland.sh`.
|
||||||
|
|
||||||
|
hl.env("DMS_RUN_GREETER", "1")
|
||||||
|
|
||||||
|
hl.on("hyprland.start", function()
|
||||||
|
hl.exec_cmd('sh -c "qs -p _DMS_PATH_; hyprctl dispatch exit"')
|
||||||
|
end)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user