mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-17 16:45:19 -04:00
Compare commits
3 Commits
e0ab0a6b90
...
e50ac208e3
| Author | SHA1 | Date | |
|---|---|---|---|
| e50ac208e3 | |||
| bcb5617194 | |||
| d3c23ba737 |
@@ -294,7 +294,14 @@ func runSetup() error {
|
|||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := true
|
||||||
|
if wmSelected {
|
||||||
|
if wm == deps.WindowManagerMango {
|
||||||
|
useSystemd = false
|
||||||
|
} else {
|
||||||
|
useSystemd = promptSystemd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !wmSelected && !terminalSelected {
|
if !wmSelected && !terminalSelected {
|
||||||
fmt.Println("No configurations selected. Exiting.")
|
fmt.Println("No configurations selected. Exiting.")
|
||||||
|
|||||||
@@ -520,6 +520,18 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMangoConfigStructure(t *testing.T) {
|
||||||
|
assert.Contains(t, MangoConfig, "exec-once=dms run")
|
||||||
|
assert.NotContains(t, MangoConfig, "exec_once=dms run")
|
||||||
|
assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,K,focusdir,up")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "bind=SUPER,L,focusdir,right")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "gesturebind=none,right,3,viewtoleft_have_client")
|
||||||
|
assert.Contains(t, MangoBindsConfig, "gesturebind=none,left,3,viewtoright_have_client")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
|
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
|
||||||
# Format: bind=MODS,key,action[,args]
|
# Format: bind=MODS,key,action[,args]
|
||||||
# Descriptions go on the line ABOVE each bind (mango does not strip inline
|
# Put bind descriptions above bind lines; inline # comments break Mango spawn args.
|
||||||
# comments — a trailing `# ...` would be passed to spawn as extra arguments).
|
|
||||||
|
|
||||||
# === Application Launchers ===
|
# === Application Launchers ===
|
||||||
# Open Terminal
|
# Open Terminal
|
||||||
@@ -52,131 +51,90 @@ bind=CTRL,Print,spawn,dms screenshot full
|
|||||||
bind=ALT,Print,spawn,dms screenshot window
|
bind=ALT,Print,spawn,dms screenshot window
|
||||||
|
|
||||||
# === Audio Controls ===
|
# === Audio Controls ===
|
||||||
# Volume Up
|
|
||||||
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
|
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
|
||||||
# Volume Down
|
|
||||||
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
|
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
|
||||||
# Mute Output
|
|
||||||
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
|
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
|
||||||
# Mute Microphone
|
|
||||||
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
|
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
|
||||||
# Play/Pause
|
|
||||||
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
|
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
|
||||||
# Play/Pause
|
|
||||||
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
|
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
|
||||||
# Previous Track
|
|
||||||
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
|
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
|
||||||
# Next Track
|
|
||||||
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
|
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
|
||||||
|
|
||||||
# === Brightness Controls ===
|
# === Brightness Controls ===
|
||||||
# Brightness Up
|
|
||||||
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
|
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
|
||||||
# Brightness Down
|
|
||||||
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
|
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
|
||||||
|
|
||||||
# === Window Management ===
|
# === Window Management ===
|
||||||
# Close Window
|
# Close Window
|
||||||
bind=SUPER,q,killclient,
|
bind=SUPER,q,killclient,
|
||||||
# Toggle Fullscreen
|
|
||||||
bind=SUPER,f,togglefullscreen,
|
bind=SUPER,f,togglefullscreen,
|
||||||
# Toggle Maximize
|
|
||||||
bind=SUPER,a,togglemaximizescreen,
|
bind=SUPER,a,togglemaximizescreen,
|
||||||
# Toggle Floating
|
|
||||||
bind=SUPER+SHIFT,space,togglefloating,
|
bind=SUPER+SHIFT,space,togglefloating,
|
||||||
# Toggle Overview
|
|
||||||
bind=SUPER,o,toggleoverview
|
bind=SUPER,o,toggleoverview
|
||||||
bind=ALT,Tab,toggleoverview
|
bind=ALT,Tab,toggleoverview
|
||||||
# Exit Compositor
|
# Exit Compositor
|
||||||
bind=SUPER+SHIFT,e,quit,
|
bind=SUPER+SHIFT,e,quit,
|
||||||
|
|
||||||
# === Focus Navigation ===
|
# === Focus Navigation ===
|
||||||
# Focus Next Window
|
|
||||||
bind=SUPER,Tab,focusstack,next
|
bind=SUPER,Tab,focusstack,next
|
||||||
# Focus Previous Window
|
|
||||||
bind=SUPER+SHIFT,Tab,focusstack,prev
|
bind=SUPER+SHIFT,Tab,focusstack,prev
|
||||||
# Focus Left
|
|
||||||
bind=SUPER,Left,focusdir,left
|
bind=SUPER,Left,focusdir,left
|
||||||
# Focus Right
|
bind=SUPER,H,focusdir,left
|
||||||
bind=SUPER,Right,focusdir,right
|
bind=SUPER,Right,focusdir,right
|
||||||
# Focus Up
|
bind=SUPER,L,focusdir,right
|
||||||
bind=SUPER,Up,focusdir,up
|
bind=SUPER,Up,focusdir,up
|
||||||
# Focus Down
|
bind=SUPER,K,focusdir,up
|
||||||
bind=SUPER,Down,focusdir,down
|
bind=SUPER,Down,focusdir,down
|
||||||
|
bind=SUPER,J,focusdir,down
|
||||||
|
|
||||||
# === Window Movement ===
|
# === Window Movement ===
|
||||||
# Move Window Left
|
|
||||||
bind=SUPER+SHIFT,Left,exchange_client,left
|
bind=SUPER+SHIFT,Left,exchange_client,left
|
||||||
# Move Window Right
|
|
||||||
bind=SUPER+SHIFT,Right,exchange_client,right
|
bind=SUPER+SHIFT,Right,exchange_client,right
|
||||||
# Move Window Up
|
|
||||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||||
# Move Window Down
|
|
||||||
bind=SUPER+SHIFT,Down,exchange_client,down
|
bind=SUPER+SHIFT,Down,exchange_client,down
|
||||||
|
bind=SUPER+SHIFT,H,exchange_client,left
|
||||||
|
bind=SUPER+SHIFT,L,exchange_client,right
|
||||||
|
bind=SUPER+SHIFT,K,exchange_client,up
|
||||||
|
bind=SUPER+SHIFT,J,exchange_client,down
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
# === Monitor Navigation ===
|
||||||
# Focus Monitor Left
|
|
||||||
bind=SUPER+ALT,Left,focusmon,left
|
bind=SUPER+ALT,Left,focusmon,left
|
||||||
# Focus Monitor Right
|
|
||||||
bind=SUPER+ALT,Right,focusmon,right
|
bind=SUPER+ALT,Right,focusmon,right
|
||||||
# Move to Monitor Left
|
|
||||||
bind=SUPER+ALT+SHIFT,Left,tagmon,left
|
bind=SUPER+ALT+SHIFT,Left,tagmon,left
|
||||||
# Move to Monitor Right
|
|
||||||
bind=SUPER+ALT+SHIFT,Right,tagmon,right
|
bind=SUPER+ALT+SHIFT,Right,tagmon,right
|
||||||
|
|
||||||
# === Layout ===
|
# === Layout ===
|
||||||
# Cycle Layout
|
# Cycle Layout - Gaps, Floating, Tiling
|
||||||
bind=SUPER,j,switch_layout
|
bind=SUPER+ALT,j,switch_layout
|
||||||
# Increase Gaps
|
|
||||||
bind=SUPER+SHIFT,equal,incgaps,1
|
bind=SUPER+SHIFT,equal,incgaps,1
|
||||||
# Decrease Gaps
|
|
||||||
bind=SUPER+SHIFT,minus,incgaps,-1
|
bind=SUPER+SHIFT,minus,incgaps,-1
|
||||||
|
|
||||||
# === Tags (1-9): view tag ===
|
# === Tags (1-9): view tag ===
|
||||||
# View Tag 1
|
|
||||||
bind=SUPER,1,view,1
|
bind=SUPER,1,view,1
|
||||||
# View Tag 2
|
|
||||||
bind=SUPER,2,view,2
|
bind=SUPER,2,view,2
|
||||||
# View Tag 3
|
|
||||||
bind=SUPER,3,view,3
|
bind=SUPER,3,view,3
|
||||||
# View Tag 4
|
|
||||||
bind=SUPER,4,view,4
|
bind=SUPER,4,view,4
|
||||||
# View Tag 5
|
|
||||||
bind=SUPER,5,view,5
|
bind=SUPER,5,view,5
|
||||||
# View Tag 6
|
|
||||||
bind=SUPER,6,view,6
|
bind=SUPER,6,view,6
|
||||||
# View Tag 7
|
|
||||||
bind=SUPER,7,view,7
|
bind=SUPER,7,view,7
|
||||||
# View Tag 8
|
|
||||||
bind=SUPER,8,view,8
|
bind=SUPER,8,view,8
|
||||||
# View Tag 9
|
|
||||||
bind=SUPER,9,view,9
|
bind=SUPER,9,view,9
|
||||||
|
|
||||||
# === Tags (1-9): move focused window to tag ===
|
# === Tags (1-9): move focused window to tag ===
|
||||||
# Move to Tag 1
|
|
||||||
bind=SUPER+SHIFT,1,tag,1
|
bind=SUPER+SHIFT,1,tag,1
|
||||||
# Move to Tag 2
|
|
||||||
bind=SUPER+SHIFT,2,tag,2
|
bind=SUPER+SHIFT,2,tag,2
|
||||||
# Move to Tag 3
|
|
||||||
bind=SUPER+SHIFT,3,tag,3
|
bind=SUPER+SHIFT,3,tag,3
|
||||||
# Move to Tag 4
|
|
||||||
bind=SUPER+SHIFT,4,tag,4
|
bind=SUPER+SHIFT,4,tag,4
|
||||||
# Move to Tag 5
|
|
||||||
bind=SUPER+SHIFT,5,tag,5
|
bind=SUPER+SHIFT,5,tag,5
|
||||||
# Move to Tag 6
|
|
||||||
bind=SUPER+SHIFT,6,tag,6
|
bind=SUPER+SHIFT,6,tag,6
|
||||||
# Move to Tag 7
|
|
||||||
bind=SUPER+SHIFT,7,tag,7
|
bind=SUPER+SHIFT,7,tag,7
|
||||||
# Move to Tag 8
|
|
||||||
bind=SUPER+SHIFT,8,tag,8
|
bind=SUPER+SHIFT,8,tag,8
|
||||||
# Move to Tag 9
|
|
||||||
bind=SUPER+SHIFT,9,tag,9
|
bind=SUPER+SHIFT,9,tag,9
|
||||||
|
|
||||||
# === Touchpad Gestures ===
|
# === Touchpad Gestures ===
|
||||||
# Syntax: gesturebind=MODIFIERS,DIRECTION,FINGERS,COMMAND,PARAMETERS
|
|
||||||
# 3-finger horizontal swipe: switch between occupied workspaces
|
# 3-finger horizontal swipe: switch between occupied workspaces
|
||||||
gesturebind=none,left,3,viewtoleft_have_client
|
gesturebind=none,right,3,viewtoleft_have_client
|
||||||
gesturebind=none,right,3,viewtoright_have_client
|
gesturebind=none,left,3,viewtoright_have_client
|
||||||
# 4-finger vertical swipe: toggle the overview
|
# 4-finger vertical swipe: toggle the overview
|
||||||
gesturebind=none,up,4,toggleoverview
|
gesturebind=none,up,4,toggleoverview
|
||||||
gesturebind=none,down,4,toggleoverview
|
gesturebind=none,down,4,toggleoverview
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
env=XDG_CURRENT_DESKTOP,mango
|
env=XDG_CURRENT_DESKTOP,mango
|
||||||
env=XDG_SESSION_TYPE,wayland
|
env=XDG_SESSION_TYPE,wayland
|
||||||
|
|
||||||
# exec_once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
||||||
# every exec= on each config reload, and DMS reloads the config, which would
|
# every exec= on each config reload, and DMS reloads the config, which would
|
||||||
# spawn a new shell on every reload.
|
# spawn a new shell on every reload.
|
||||||
exec_once=dms run
|
exec-once=dms run
|
||||||
|
|
||||||
source=./dms/colors.conf
|
source=./dms/colors.conf
|
||||||
source=./dms/layout.conf
|
source=./dms/layout.conf
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
@@ -228,11 +229,20 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
normalizedKey := strings.ToLower(key)
|
||||||
|
prefix := "bind"
|
||||||
|
if existing, ok := existingBinds[normalizedKey]; ok && existing.Prefix != "" {
|
||||||
|
prefix = existing.Prefix
|
||||||
|
}
|
||||||
|
if optionPrefix := m.bindPrefixFromOptions(options); optionPrefix != "" {
|
||||||
|
prefix = optionPrefix
|
||||||
|
}
|
||||||
|
|
||||||
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
||||||
Key: key,
|
Key: key,
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: description,
|
Description: description,
|
||||||
Options: options,
|
Options: options,
|
||||||
|
Prefix: prefix,
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.writeOverrideBinds(existingBinds)
|
return m.writeOverrideBinds(existingBinds)
|
||||||
@@ -246,7 +256,7 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
|
|||||||
|
|
||||||
normalizedKey := strings.ToLower(key)
|
normalizedKey := strings.ToLower(key)
|
||||||
delete(existingBinds, normalizedKey)
|
delete(existingBinds, normalizedKey)
|
||||||
return m.writeOverrideBinds(existingBinds)
|
return m.writeOverrideBindsWithRemoved(existingBinds, map[string]bool{normalizedKey: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) ResetBind(key string) error {
|
func (m *MangoWCProvider) ResetBind(key string) error {
|
||||||
@@ -258,6 +268,7 @@ type mangowcOverrideBind struct {
|
|||||||
Action string
|
Action string
|
||||||
Description string
|
Description string
|
||||||
Options map[string]any
|
Options map[string]any
|
||||||
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
|
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
|
||||||
@@ -272,34 +283,63 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
var pendingComment string
|
||||||
for _, line := range lines {
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
line = strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if trimmed == "" {
|
||||||
|
pendingComment = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "#") {
|
||||||
|
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||||
|
if isMangoWCSectionComment(pendingComment) {
|
||||||
|
pendingComment = ""
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(line, "bind") {
|
bind, ok := m.parseOverrideBindLine(line, pendingComment)
|
||||||
|
pendingComment = ""
|
||||||
|
if !ok || bind == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
binds[strings.ToLower(bind.Key)] = bind
|
||||||
|
}
|
||||||
|
|
||||||
|
return binds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) parseOverrideBindLine(line, precedingComment string) (*mangowcOverrideBind, bool) {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
continue
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := strings.TrimSpace(parts[0])
|
||||||
|
if !m.isBindPrefix(prefix) {
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
content := strings.TrimSpace(parts[1])
|
content := strings.TrimSpace(parts[1])
|
||||||
commentParts := strings.SplitN(content, "#", 2)
|
commentParts := strings.SplitN(content, "#", 2)
|
||||||
bindContent := strings.TrimSpace(commentParts[0])
|
bindContent := strings.TrimSpace(commentParts[0])
|
||||||
|
|
||||||
var comment string
|
description := strings.TrimSpace(precedingComment)
|
||||||
|
if isMangoWCSectionComment(description) {
|
||||||
|
description = ""
|
||||||
|
}
|
||||||
if len(commentParts) > 1 {
|
if len(commentParts) > 1 {
|
||||||
comment = strings.TrimSpace(commentParts[1])
|
description = strings.TrimSpace(commentParts[1])
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(description, MangoWCHideComment) {
|
||||||
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.SplitN(bindContent, ",", 4)
|
fields := strings.SplitN(bindContent, ",", 4)
|
||||||
if len(fields) < 3 {
|
if len(fields) < 3 {
|
||||||
continue
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := strings.TrimSpace(fields[0])
|
mods := strings.TrimSpace(fields[0])
|
||||||
@@ -311,21 +351,29 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
|
|||||||
params = strings.TrimSpace(fields[3])
|
params = strings.TrimSpace(fields[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
keyStr := m.buildKeyString(mods, keyName)
|
|
||||||
normalizedKey := strings.ToLower(keyStr)
|
|
||||||
action := command
|
action := command
|
||||||
if params != "" {
|
if params != "" {
|
||||||
action = command + " " + params
|
action = command + " " + params
|
||||||
}
|
}
|
||||||
|
|
||||||
binds[normalizedKey] = &mangowcOverrideBind{
|
return &mangowcOverrideBind{
|
||||||
Key: keyStr,
|
Key: m.buildKeyString(mods, keyName),
|
||||||
Action: action,
|
Action: action,
|
||||||
Description: comment,
|
Description: description,
|
||||||
}
|
Prefix: prefix,
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return binds, nil
|
func (m *MangoWCProvider) isBindPrefix(prefix string) bool {
|
||||||
|
if !strings.HasPrefix(prefix, "bind") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ch := range strings.TrimPrefix(prefix, "bind") {
|
||||||
|
if !strings.ContainsRune("lsrp", ch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
|
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
|
||||||
@@ -362,21 +410,113 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||||
|
return m.writeOverrideBindsWithRemoved(binds, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) writeOverrideBindsWithRemoved(binds map[string]*mangowcOverrideBind, removed map[string]bool) error {
|
||||||
overridePath := m.GetOverridePath()
|
overridePath := m.GetOverridePath()
|
||||||
content := m.generateBindsContent(binds)
|
existingContent := ""
|
||||||
|
if data, err := os.ReadFile(overridePath); err == nil {
|
||||||
|
existingContent = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := m.generatePreservedBindsContent(existingContent, binds, removed)
|
||||||
return os.WriteFile(overridePath, []byte(content), 0o644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
func (m *MangoWCProvider) generatePreservedBindsContent(existingContent string, binds map[string]*mangowcOverrideBind, removed map[string]bool) string {
|
||||||
if len(binds) == 0 {
|
useStockScaffold := m.shouldUseStockScaffold(existingContent)
|
||||||
return ""
|
source := existingContent
|
||||||
|
if useStockScaffold {
|
||||||
|
source = m.stockBindsScaffold(binds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remaining := make(map[string]*mangowcOverrideBind, len(binds))
|
||||||
|
for key, bind := range binds {
|
||||||
|
remaining[key] = bind
|
||||||
|
}
|
||||||
|
if useStockScaffold {
|
||||||
|
m.dropReplacedStockBinds(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
for _, line := range strings.Split(source, "\n") {
|
||||||
|
templateBind, ok := m.parseOverrideBindLine(line, m.previousComment(lines))
|
||||||
|
if !ok || templateBind == nil {
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedKey := strings.ToLower(templateBind.Key)
|
||||||
|
m.dropPreviousDescriptionComment(&lines)
|
||||||
|
|
||||||
|
if bind, exists := remaining[normalizedKey]; exists {
|
||||||
|
if useStockScaffold && bind.Description == "" {
|
||||||
|
bind = m.copyBindWithDescription(bind, templateBind.Description)
|
||||||
|
}
|
||||||
|
m.writeBindLineToLines(&lines, bind)
|
||||||
|
delete(remaining, normalizedKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if useStockScaffold && !removed[normalizedKey] {
|
||||||
|
m.writeBindLineToLines(&lines, templateBind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remaining) > 0 {
|
||||||
|
m.trimTrailingEmptyLines(&lines)
|
||||||
|
if len(lines) > 0 {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
lines = append(lines, "# === Custom Keybinds ===")
|
||||||
|
for _, bind := range m.sortedBinds(remaining) {
|
||||||
|
m.writeBindLineToLines(&lines, bind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.trimTrailingEmptyLines(&lines)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) shouldUseStockScaffold(content string) bool {
|
||||||
|
if strings.TrimSpace(content) == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "gesturebind=") && strings.Contains(content, "# ===") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !strings.Contains(content, "gesturebind=") && (strings.Count(content, "\nbind=")+strings.Count(content, "\nbindl=")+strings.Count(content, "\nbinds=")+strings.Count(content, "\nbindr=")+strings.Count(content, "\nbindp=") >= 10 || strings.Contains(content, "dms ipc call"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) stockBindsScaffold(binds map[string]*mangowcOverrideBind) string {
|
||||||
|
terminalCommand := "ghostty"
|
||||||
|
for _, key := range []string{"super+t", "super+return"} {
|
||||||
|
if bind, ok := binds[key]; ok {
|
||||||
|
command, params := m.parseAction(bind.Action)
|
||||||
|
if command == "spawn" && strings.TrimSpace(params) != "" && !strings.Contains(params, "dms ") {
|
||||||
|
terminalCommand = params
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) dropReplacedStockBinds(binds map[string]*mangowcOverrideBind) {
|
||||||
|
if bind, ok := binds["super+j"]; ok && bind.Action == "switch_layout" {
|
||||||
|
delete(binds, "super+j")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) sortedBinds(binds map[string]*mangowcOverrideBind) []*mangowcOverrideBind {
|
||||||
bindList := make([]*mangowcOverrideBind, 0, len(binds))
|
bindList := make([]*mangowcOverrideBind, 0, len(binds))
|
||||||
for _, bind := range binds {
|
for _, bind := range binds {
|
||||||
bindList = append(bindList, bind)
|
bindList = append(bindList, bind)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(bindList, func(i, j int) bool {
|
sort.Slice(bindList, func(i, j int) bool {
|
||||||
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
|
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
|
||||||
if pi != pj {
|
if pi != pj {
|
||||||
@@ -384,13 +524,55 @@ func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverride
|
|||||||
}
|
}
|
||||||
return bindList[i].Key < bindList[j].Key
|
return bindList[i].Key < bindList[j].Key
|
||||||
})
|
})
|
||||||
|
return bindList
|
||||||
var sb strings.Builder
|
|
||||||
for _, bind := range bindList {
|
|
||||||
m.writeBindLine(&sb, bind)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
func (m *MangoWCProvider) writeBindLineToLines(lines *[]string, bind *mangowcOverrideBind) {
|
||||||
|
var sb strings.Builder
|
||||||
|
m.writeBindLine(&sb, bind)
|
||||||
|
text := strings.TrimSuffix(sb.String(), "\n")
|
||||||
|
if text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*lines = append(*lines, strings.Split(text, "\n")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) previousComment(lines []string) string {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace(lines[len(lines)-1])
|
||||||
|
if !strings.HasPrefix(trimmed, "#") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
comment := strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||||
|
if isMangoWCSectionComment(comment) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return comment
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) dropPreviousDescriptionComment(lines *[]string) {
|
||||||
|
if len(*lines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace((*lines)[len(*lines)-1])
|
||||||
|
if !strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "# ===") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*lines = (*lines)[:len(*lines)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) trimTrailingEmptyLines(lines *[]string) {
|
||||||
|
for len(*lines) > 0 && strings.TrimSpace((*lines)[len(*lines)-1]) == "" {
|
||||||
|
*lines = (*lines)[:len(*lines)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) copyBindWithDescription(bind *mangowcOverrideBind, description string) *mangowcOverrideBind {
|
||||||
|
copy := *bind
|
||||||
|
copy.Description = description
|
||||||
|
return ©
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
|
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
|
||||||
@@ -405,7 +587,12 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString("bind=")
|
prefix := bind.Prefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = "bind"
|
||||||
|
}
|
||||||
|
sb.WriteString(prefix)
|
||||||
|
sb.WriteString("=")
|
||||||
if mods == "" {
|
if mods == "" {
|
||||||
sb.WriteString("none")
|
sb.WriteString("none")
|
||||||
} else {
|
} else {
|
||||||
@@ -424,6 +611,36 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MangoWCProvider) bindPrefixFromOptions(options map[string]any) string {
|
||||||
|
if options == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
value, ok := options["flags"]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
flags := ""
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
flags = v
|
||||||
|
case fmt.Stringer:
|
||||||
|
flags = v.String()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
flags = strings.TrimSpace(flags)
|
||||||
|
if flags == "" {
|
||||||
|
return "bind"
|
||||||
|
}
|
||||||
|
var clean strings.Builder
|
||||||
|
for _, ch := range flags {
|
||||||
|
if strings.ContainsRune("lsrp", ch) && !strings.ContainsRune(clean.String(), ch) {
|
||||||
|
clean.WriteRune(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "bind" + clean.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
|
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||||
parts := strings.Split(keyStr, "+")
|
parts := strings.Split(keyStr, "+")
|
||||||
switch len(parts) {
|
switch len(parts) {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const (
|
|||||||
|
|
||||||
var MangoWCModSeparators = []rune{'+', ' '}
|
var MangoWCModSeparators = []rune{'+', ' '}
|
||||||
|
|
||||||
|
func isMangoWCSectionComment(comment string) bool {
|
||||||
|
return strings.HasPrefix(strings.TrimSpace(comment), "===")
|
||||||
|
}
|
||||||
|
|
||||||
type MangoWCKeyBinding struct {
|
type MangoWCKeyBinding struct {
|
||||||
Mods []string `json:"mods"`
|
Mods []string `json:"mods"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
@@ -235,6 +239,9 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
|||||||
}
|
}
|
||||||
if strings.HasPrefix(trimmed, "#") {
|
if strings.HasPrefix(trimmed, "#") {
|
||||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||||
|
if isMangoWCSectionComment(pendingComment) {
|
||||||
|
pendingComment = ""
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(trimmed, "bind") {
|
if !strings.HasPrefix(trimmed, "bind") {
|
||||||
@@ -414,6 +421,9 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
|
|||||||
|
|
||||||
if strings.HasPrefix(trimmed, "#") {
|
if strings.HasPrefix(trimmed, "#") {
|
||||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||||
|
if isMangoWCSectionComment(pendingComment) {
|
||||||
|
pendingComment = ""
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,7 +493,7 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
|
|||||||
// line directly above) is the description: mango feeds inline comments to spawn
|
// line directly above) is the description: mango feeds inline comments to spawn
|
||||||
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
|
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
|
||||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
|
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
|
||||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
bindMatch := regexp.MustCompile(`^(bind[lsrp]*)\s*=\s*(.+)$`)
|
||||||
matches := bindMatch.FindStringSubmatch(line)
|
matches := bindMatch.FindStringSubmatch(line)
|
||||||
if len(matches) < 3 {
|
if len(matches) < 3 {
|
||||||
return nil
|
return nil
|
||||||
@@ -499,6 +509,9 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment st
|
|||||||
}
|
}
|
||||||
if comment == "" {
|
if comment == "" {
|
||||||
comment = strings.TrimSpace(precedingComment)
|
comment = strings.TrimSpace(precedingComment)
|
||||||
|
if isMangoWCSectionComment(comment) {
|
||||||
|
comment = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
line string
|
line string
|
||||||
|
precedingComment string
|
||||||
expected *MangoWCKeyBinding
|
expected *MangoWCKeyBinding
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -157,6 +158,41 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
|||||||
Comment: "dms ipc call lock lock",
|
Comment: "dms ipc call lock lock",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "bindp_flag",
|
||||||
|
line: "bindp=SUPER,p,spawn,pass-through",
|
||||||
|
expected: &MangoWCKeyBinding{
|
||||||
|
Mods: []string{"SUPER"},
|
||||||
|
Key: "p",
|
||||||
|
Command: "spawn",
|
||||||
|
Params: "pass-through",
|
||||||
|
Comment: "pass-through",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preceding_comment",
|
||||||
|
line: "bind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||||
|
precedingComment: "Screenshot: Interactive",
|
||||||
|
expected: &MangoWCKeyBinding{
|
||||||
|
Mods: []string{"SUPER", "SHIFT"},
|
||||||
|
Key: "S",
|
||||||
|
Command: "spawn",
|
||||||
|
Params: "dms screenshot",
|
||||||
|
Comment: "Screenshot: Interactive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "section_header_not_description",
|
||||||
|
line: "bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3",
|
||||||
|
precedingComment: "=== Audio Controls ===",
|
||||||
|
expected: &MangoWCKeyBinding{
|
||||||
|
Mods: []string{},
|
||||||
|
Key: "XF86AudioRaiseVolume",
|
||||||
|
Command: "spawn",
|
||||||
|
Params: "dms ipc call audio increment 3",
|
||||||
|
Comment: "dms ipc call audio increment 3",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "keybind_with_spaces",
|
name: "keybind_with_spaces",
|
||||||
line: "bind = SUPER, r, reload_config",
|
line: "bind = SUPER, r, reload_config",
|
||||||
@@ -174,7 +210,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
parser := NewMangoWCParser("")
|
parser := NewMangoWCParser("")
|
||||||
parser.contentLines = []string{tt.line}
|
parser.contentLines = []string{tt.line}
|
||||||
result := parser.getKeybindAtLine(0, "")
|
result := parser.getKeybindAtLine(0, tt.precedingComment)
|
||||||
|
|
||||||
if tt.expected == nil {
|
if tt.expected == nil {
|
||||||
if result != nil {
|
if result != nil {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMangoWCProviderName(t *testing.T) {
|
func TestMangoWCProviderName(t *testing.T) {
|
||||||
@@ -318,3 +321,138 @@ bind=Ctrl,1,view,1,0
|
|||||||
t.Error("Did not find terminal keybind with correct key and description")
|
t.Error("Did not find terminal keybind with correct key and description")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMangoWCSetBindPreservesStockCommentsAndGestures(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)
|
||||||
|
}
|
||||||
|
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||||
|
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
|
||||||
|
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write stock binds: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewMangoWCProvider(tmpDir)
|
||||||
|
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
|
||||||
|
t.Fatalf("SetBind failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBytes, err := os.ReadFile(bindsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read binds: %v", err)
|
||||||
|
}
|
||||||
|
content := string(contentBytes)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
"# === Application Launchers ===",
|
||||||
|
"# === Touchpad Gestures ===",
|
||||||
|
"gesturebind=none,right,3,viewtoleft_have_client",
|
||||||
|
"gesturebind=none,left,3,viewtoright_have_client",
|
||||||
|
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Fatalf("expected saved binds to contain %q\ncontent:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "# === Audio Controls ===\n# === Audio Controls ===") {
|
||||||
|
t.Fatalf("section header should not be duplicated as a bind description\ncontent:\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMangoWCSetBindRestoresScaffoldForStrippedFile(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)
|
||||||
|
}
|
||||||
|
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||||
|
stripped := `bind=SUPER,t,spawn,ghostty
|
||||||
|
bind=SUPER,Return,spawn,ghostty
|
||||||
|
bind=SUPER,space,spawn,dms ipc call spotlight toggle
|
||||||
|
bind=SUPER,v,spawn,dms ipc call clipboard toggle
|
||||||
|
bind=SUPER,q,killclient
|
||||||
|
bind=SUPER,Left,focusdir,left
|
||||||
|
bind=SUPER,Right,focusdir,right
|
||||||
|
bind=SUPER,Up,focusdir,up
|
||||||
|
bind=SUPER,Down,focusdir,down
|
||||||
|
bind=SUPER,1,view,1
|
||||||
|
bind=SUPER,2,view,2
|
||||||
|
bind=SUPER,3,view,3
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(bindsPath, []byte(stripped), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write stripped binds: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewMangoWCProvider(tmpDir)
|
||||||
|
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
|
||||||
|
t.Fatalf("SetBind failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBytes, err := os.ReadFile(bindsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read binds: %v", err)
|
||||||
|
}
|
||||||
|
content := string(contentBytes)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
"# DMS default keybinds (MangoWM)",
|
||||||
|
"# === Touchpad Gestures ===",
|
||||||
|
"gesturebind=none,right,3,viewtoleft_have_client",
|
||||||
|
"bind=SUPER,H,focusdir,left",
|
||||||
|
"bind=SUPER,J,focusdir,down",
|
||||||
|
"bind=SUPER,K,focusdir,up",
|
||||||
|
"bind=SUPER,L,focusdir,right",
|
||||||
|
"# === Custom Keybinds ===",
|
||||||
|
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||||
|
"bind=SUPER,t,spawn,ghostty",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Fatalf("expected restored binds to contain %q\ncontent:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "{{TERMINAL_COMMAND}}") {
|
||||||
|
t.Fatalf("terminal placeholder should have been resolved\ncontent:\n%s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMangoWCRemoveBindPreservesNonBindLines(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)
|
||||||
|
}
|
||||||
|
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||||
|
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
|
||||||
|
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write stock binds: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewMangoWCProvider(tmpDir)
|
||||||
|
if err := provider.RemoveBind("SUPER+Tab"); err != nil {
|
||||||
|
t.Fatalf("RemoveBind failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBytes, err := os.ReadFile(bindsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read binds: %v", err)
|
||||||
|
}
|
||||||
|
content := string(contentBytes)
|
||||||
|
|
||||||
|
if strings.Contains(content, "bind=SUPER,Tab,focusstack,next") {
|
||||||
|
t.Fatalf("removed bind should be absent\ncontent:\n%s", content)
|
||||||
|
}
|
||||||
|
if strings.Contains(content, "# Focus Next Window") {
|
||||||
|
t.Fatalf("removed bind description should be absent\ncontent:\n%s", content)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"# === Focus Navigation ===",
|
||||||
|
"# === Touchpad Gestures ===",
|
||||||
|
"gesturebind=none,down,4,toggleoverview",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(content, want) {
|
||||||
|
t.Fatalf("expected non-bind line %q to be preserved\ncontent:\n%s", want, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func (m Model) viewInstallComplete() string {
|
|||||||
|
|
||||||
wm := m.selectedWindowManager()
|
wm := m.selectedWindowManager()
|
||||||
|
|
||||||
// mango launches DMS via `exec_once=dms run` (not a systemd session target)
|
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
|
||||||
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
@@ -223,7 +223,7 @@ func (m Model) viewInstallComplete() string {
|
|||||||
|
|
||||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
||||||
if wm == deps.WindowManagerMango {
|
if wm == deps.WindowManagerMango {
|
||||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec_once=dms run' from ~/.config/mango/config.conf") + "\n")
|
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||||
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
|
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ Singleton {
|
|||||||
property int mangoLayoutGapsOverride: -1
|
property int mangoLayoutGapsOverride: -1
|
||||||
property int mangoLayoutRadiusOverride: -1
|
property int mangoLayoutRadiusOverride: -1
|
||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
|
property bool mangoTrackpadNaturalScrolling: true
|
||||||
|
|
||||||
property int firstDayOfWeek: -1
|
property int firstDayOfWeek: -1
|
||||||
property bool showWeekNumber: false
|
property bool showWeekNumber: false
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ var SPEC = {
|
|||||||
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
||||||
|
|
||||||
firstDayOfWeek: { def: -1 },
|
firstDayOfWeek: { def: -1 },
|
||||||
showWeekNumber: { def: false },
|
showWeekNumber: { def: false },
|
||||||
@@ -237,7 +238,7 @@ var SPEC = {
|
|||||||
qt6ctAvailable: { def: false, persist: false },
|
qt6ctAvailable: { def: false, persist: false },
|
||||||
gtkAvailable: { def: false, persist: false },
|
gtkAvailable: { def: false, persist: false },
|
||||||
|
|
||||||
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
|
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 }, mango: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
|
||||||
availableCursorThemes: { def: ["System Default"], persist: false },
|
availableCursorThemes: { def: ["System Default"], persist: false },
|
||||||
systemDefaultCursorTheme: { def: "", persist: false },
|
systemDefaultCursorTheme: { def: "", persist: false },
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ Item {
|
|||||||
return root.screenName;
|
return root.screenName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readonly property bool mangoOverviewActive: CompositorService.isMango && MangoService.isOutputInOverview(effectiveScreenName)
|
||||||
|
|
||||||
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
|
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
|
||||||
readonly property bool useExtWorkspace: {
|
readonly property bool useExtWorkspace: {
|
||||||
@@ -160,7 +161,11 @@ Item {
|
|||||||
baseList = getHyprlandWorkspaces();
|
baseList = getHyprlandWorkspaces();
|
||||||
break;
|
break;
|
||||||
case "dwl":
|
case "dwl":
|
||||||
|
baseList = getDwlTags();
|
||||||
|
break;
|
||||||
case "mango":
|
case "mango":
|
||||||
|
if (root.mangoOverviewActive)
|
||||||
|
return [];
|
||||||
baseList = getDwlTags();
|
baseList = getDwlTags();
|
||||||
break;
|
break;
|
||||||
case "sway":
|
case "sway":
|
||||||
@@ -977,7 +982,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: !root.isVertical
|
visible: !root.isVertical
|
||||||
text: I18n.tr("OVERVIEW")
|
text: I18n.tr("Overview")
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
font.pixelSize: overviewPill.labelSize
|
font.pixelSize: overviewPill.labelSize
|
||||||
font.weight: Font.DemiBold
|
font.weight: Font.DemiBold
|
||||||
@@ -1115,7 +1120,7 @@ Item {
|
|||||||
targetWorkspaceId = modelData?.id;
|
targetWorkspaceId = modelData?.id;
|
||||||
} else if (CompositorService.isHyprland) {
|
} else if (CompositorService.isHyprland) {
|
||||||
targetWorkspaceId = modelData?.id;
|
targetWorkspaceId = modelData?.id;
|
||||||
} else if (CompositorService.isDwl) {
|
} else if (root.isDwlLike) {
|
||||||
targetWorkspaceId = modelData?.tag;
|
targetWorkspaceId = modelData?.tag;
|
||||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||||
targetWorkspaceId = modelData?.num;
|
targetWorkspaceId = modelData?.num;
|
||||||
|
|||||||
@@ -2432,6 +2432,17 @@ Item {
|
|||||||
onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue)
|
onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tab: "theme"
|
||||||
|
tags: ["mango", "touchpad", "trackpad", "natural", "scrolling"]
|
||||||
|
settingKey: "mangoTrackpadNaturalScrolling"
|
||||||
|
text: I18n.tr("Natural Touchpad Scrolling")
|
||||||
|
description: I18n.tr("Invert touchpad scroll direction")
|
||||||
|
visible: CompositorService.isMango
|
||||||
|
checked: SettingsData.mangoTrackpadNaturalScrolling
|
||||||
|
onToggled: checked => SettingsData.set("mangoTrackpadNaturalScrolling", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
tab: "theme"
|
tab: "theme"
|
||||||
tags: ["cursor", "hide", "typing"]
|
tags: ["cursor", "hide", "typing"]
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ Item {
|
|||||||
settingKey: "dwlShowAllTags"
|
settingKey: "dwlShowAllTags"
|
||||||
tags: ["dwl", "tags", "workspace"]
|
tags: ["dwl", "tags", "workspace"]
|
||||||
text: I18n.tr("Show All Tags")
|
text: I18n.tr("Show All Tags")
|
||||||
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)")
|
description: I18n.tr("Show all 9 tags instead of only occupied tags")
|
||||||
checked: SettingsData.dwlShowAllTags
|
checked: SettingsData.dwlShowAllTags
|
||||||
visible: CompositorService.isDwl || CompositorService.isMango
|
visible: CompositorService.isDwl || CompositorService.isMango
|
||||||
onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
|
onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
|
||||||
|
PluginComponent {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
layerNamespacePlugin: "composite-example"
|
||||||
|
|
||||||
|
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
|
||||||
|
property int cycleInterval: pluginData.cycleInterval || 3000
|
||||||
|
property int maxBarEmojis: pluginData.maxBarEmojis || 3
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property var displayedEmojis: []
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: root.cycleInterval
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (root.enabledEmojis.length > 0) {
|
||||||
|
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length;
|
||||||
|
root.updateDisplayedEmojis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDisplayedEmojis() {
|
||||||
|
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length);
|
||||||
|
let emojis = [];
|
||||||
|
for (let i = 0; i < maxToShow; i++) {
|
||||||
|
const idx = (root.currentIndex + i) % root.enabledEmojis.length;
|
||||||
|
emojis.push(root.enabledEmojis[idx]);
|
||||||
|
}
|
||||||
|
root.displayedEmojis = emojis;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
updateDisplayedEmojis();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnabledEmojisChanged: updateDisplayedEmojis()
|
||||||
|
onMaxBarEmojisChanged: updateDisplayedEmojis()
|
||||||
|
|
||||||
|
horizontalBarPill: Component {
|
||||||
|
Row {
|
||||||
|
id: emojiRow
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.displayedEmojis
|
||||||
|
StyledText {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verticalBarPill: Component {
|
||||||
|
Column {
|
||||||
|
id: emojiColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.displayedEmojis
|
||||||
|
StyledText {
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popoutContent: Component {
|
||||||
|
PopoutComponent {
|
||||||
|
id: popoutColumn
|
||||||
|
|
||||||
|
headerText: "Emoji Picker"
|
||||||
|
detailsText: "Click an emoji to copy it to clipboard"
|
||||||
|
showCloseButton: true
|
||||||
|
|
||||||
|
property var allEmojis: ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥", "😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘"]
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
|
||||||
|
|
||||||
|
DankGridView {
|
||||||
|
id: emojiGrid
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.floor(parent.width / 50) * 50
|
||||||
|
height: parent.height
|
||||||
|
clip: true
|
||||||
|
cellWidth: 50
|
||||||
|
cellHeight: 50
|
||||||
|
model: popoutColumn.allEmojis
|
||||||
|
|
||||||
|
delegate: StyledRect {
|
||||||
|
width: 45
|
||||||
|
height: 45
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
|
||||||
|
border.width: 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: emojiMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["dms", "cl", "copy", modelData]);
|
||||||
|
ToastService.showInfo("Copied " + modelData + " to clipboard");
|
||||||
|
popoutColumn.closePopout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popoutWidth: 400
|
||||||
|
popoutHeight: 500
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
|
||||||
|
PluginComponent {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string scriptPath: pluginData.scriptPath || ""
|
||||||
|
property var popoutService: null
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SessionData
|
||||||
|
function onWallpaperPathChanged() {
|
||||||
|
if (scriptPath) {
|
||||||
|
var scriptProcess = scriptProcessComponent.createObject(root, {
|
||||||
|
wallpaperPath: SessionData.wallpaperPath
|
||||||
|
});
|
||||||
|
scriptProcess.running = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: scriptProcessComponent
|
||||||
|
|
||||||
|
Process {
|
||||||
|
property string wallpaperPath: ""
|
||||||
|
|
||||||
|
command: [scriptPath, wallpaperPath]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim()) {
|
||||||
|
console.log("CompositeDaemon script output:", text.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim()) {
|
||||||
|
ToastService.showError("Wallpaper Change Script Error", text.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
ToastService.showError("Wallpaper Change Script Error", "Script exited with code: " + exitCode);
|
||||||
|
}
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "compositeExample"
|
||||||
|
|
||||||
|
function runHook(): string {
|
||||||
|
if (!root.scriptPath)
|
||||||
|
return "no script configured";
|
||||||
|
var scriptProcess = scriptProcessComponent.createObject(root, {
|
||||||
|
wallpaperPath: SessionData.wallpaperPath
|
||||||
|
});
|
||||||
|
scriptProcess.running = true;
|
||||||
|
return "ran hook";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.info("CompositeDaemon: Started monitoring wallpaper changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
console.info("CompositeDaemon: Stopped monitoring wallpaper changes");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
|
||||||
|
DesktopPluginComponent {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
minWidth: 120
|
||||||
|
minHeight: 120
|
||||||
|
|
||||||
|
property bool showSeconds: pluginData.showSeconds ?? true
|
||||||
|
property bool showDate: pluginData.showDate ?? true
|
||||||
|
property string clockStyle: pluginData.clockStyle ?? "analog"
|
||||||
|
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: systemClock
|
||||||
|
precision: root.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
opacity: root.backgroundOpacity
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
sourceComponent: root.clockStyle === "digital" ? digitalClock : analogClock
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: analogClock
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: analogClockRoot
|
||||||
|
|
||||||
|
property real clockSize: Math.min(width, height) - (root.showDate ? 30 : 0)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clockFace
|
||||||
|
width: analogClockRoot.clockSize
|
||||||
|
height: analogClockRoot.clockSize
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 12
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property int index
|
||||||
|
property real markAngle: index * 30
|
||||||
|
property real markRadius: clockFace.width / 2 - 8
|
||||||
|
|
||||||
|
x: clockFace.width / 2 + markRadius * Math.sin(markAngle * Math.PI / 180) - width / 2
|
||||||
|
y: clockFace.height / 2 - markRadius * Math.cos(markAngle * Math.PI / 180) - height / 2
|
||||||
|
width: index % 3 === 0 ? 8 : 4
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: index % 3 === 0 ? Theme.primary : Theme.outlineVariant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: hourHand
|
||||||
|
property int hours: systemClock.date?.getHours() % 12 ?? 0
|
||||||
|
property int minutes: systemClock.date?.getMinutes() ?? 0
|
||||||
|
|
||||||
|
x: clockFace.width / 2 - width / 2
|
||||||
|
y: clockFace.height / 2 - height + 4
|
||||||
|
width: 6
|
||||||
|
height: clockFace.height * 0.25
|
||||||
|
radius: 3
|
||||||
|
color: Theme.primary
|
||||||
|
antialiasing: true
|
||||||
|
transformOrigin: Item.Bottom
|
||||||
|
rotation: (hours + minutes / 60) * 30
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: minuteHand
|
||||||
|
property int minutes: systemClock.date?.getMinutes() ?? 0
|
||||||
|
property int seconds: systemClock.date?.getSeconds() ?? 0
|
||||||
|
|
||||||
|
x: clockFace.width / 2 - width / 2
|
||||||
|
y: clockFace.height / 2 - height + 4
|
||||||
|
width: 4
|
||||||
|
height: clockFace.height * 0.35
|
||||||
|
radius: 2
|
||||||
|
color: Theme.onSurface
|
||||||
|
antialiasing: true
|
||||||
|
transformOrigin: Item.Bottom
|
||||||
|
rotation: (minutes + seconds / 60) * 6
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: secondHand
|
||||||
|
visible: root.showSeconds
|
||||||
|
property int seconds: systemClock.date?.getSeconds() ?? 0
|
||||||
|
|
||||||
|
x: clockFace.width / 2 - width / 2
|
||||||
|
y: clockFace.height / 2 - height + 4
|
||||||
|
width: 2
|
||||||
|
height: clockFace.height * 0.4
|
||||||
|
radius: 1
|
||||||
|
color: Theme.error
|
||||||
|
antialiasing: true
|
||||||
|
transformOrigin: Item.Bottom
|
||||||
|
rotation: seconds * 6
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 10
|
||||||
|
height: 10
|
||||||
|
radius: 5
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: root.showDate
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
|
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: digitalClock
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: digitalRoot
|
||||||
|
|
||||||
|
property real timeFontSize: Math.min(width * 0.16, height * (root.showDate ? 0.4 : 0.5))
|
||||||
|
property real dateFontSize: Math.max(Theme.fontSizeSmall, timeFontSize * 0.35)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: timeText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: root.showDate ? -digitalRoot.dateFontSize * 0.8 : 0
|
||||||
|
text: systemClock.date?.toLocaleTimeString(Qt.locale(), root.showSeconds ? "hh:mm:ss" : "hh:mm") ?? ""
|
||||||
|
font.pixelSize: digitalRoot.timeFontSize
|
||||||
|
font.weight: Font.Bold
|
||||||
|
font.family: "monospace"
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: dateText
|
||||||
|
visible: root.showDate
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: timeText.bottom
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||||
|
font.pixelSize: digitalRoot.dateFontSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Plugins
|
||||||
|
|
||||||
|
PluginSettings {
|
||||||
|
id: root
|
||||||
|
pluginId: "exampleComposite"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: "Bar Widget — Emoji Cycler"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionSetting {
|
||||||
|
settingKey: "emojiSet"
|
||||||
|
label: "Emoji Set"
|
||||||
|
description: "Choose which collection of emojis to cycle through"
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Happy & Sad",
|
||||||
|
value: "happySad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hearts",
|
||||||
|
value: "hearts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hand Gestures",
|
||||||
|
value: "hands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "All Mixed",
|
||||||
|
value: "mixed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
defaultValue: "happySad"
|
||||||
|
|
||||||
|
onValueChanged: {
|
||||||
|
const sets = {
|
||||||
|
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||||
|
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||||
|
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||||
|
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||||
|
};
|
||||||
|
root.saveValue("emojis", sets[value] || sets["happySad"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
const currentSet = value || defaultValue;
|
||||||
|
const sets = {
|
||||||
|
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
|
||||||
|
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
|
||||||
|
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
|
||||||
|
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
|
||||||
|
};
|
||||||
|
root.saveValue("emojis", sets[currentSet] || sets["happySad"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderSetting {
|
||||||
|
settingKey: "cycleInterval"
|
||||||
|
label: "Cycle Speed"
|
||||||
|
description: "How quickly emojis rotate"
|
||||||
|
defaultValue: 3000
|
||||||
|
minimum: 500
|
||||||
|
maximum: 10000
|
||||||
|
unit: "ms"
|
||||||
|
leftIcon: "schedule"
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderSetting {
|
||||||
|
settingKey: "maxBarEmojis"
|
||||||
|
label: "Max Bar Emojis"
|
||||||
|
description: "Maximum number of emojis to display in the bar at once"
|
||||||
|
defaultValue: 3
|
||||||
|
minimum: 1
|
||||||
|
maximum: 8
|
||||||
|
rightIcon: "emoji_emotions"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: "Desktop Widget — Clock"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionSetting {
|
||||||
|
settingKey: "clockStyle"
|
||||||
|
label: "Clock Style"
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Analog",
|
||||||
|
value: "analog"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Digital",
|
||||||
|
value: "digital"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
defaultValue: "analog"
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSetting {
|
||||||
|
settingKey: "showSeconds"
|
||||||
|
label: "Show Seconds"
|
||||||
|
defaultValue: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSetting {
|
||||||
|
settingKey: "showDate"
|
||||||
|
label: "Show Date"
|
||||||
|
defaultValue: true
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderSetting {
|
||||||
|
settingKey: "backgroundOpacity"
|
||||||
|
label: "Background Opacity"
|
||||||
|
defaultValue: 50
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
unit: "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: "Daemon — Wallpaper Hook"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSetting {
|
||||||
|
settingKey: "scriptPath"
|
||||||
|
label: "Script Path"
|
||||||
|
description: "Script executed when the wallpaper changes. The new wallpaper path is passed as the first argument."
|
||||||
|
placeholder: "/path/to/your/script.sh"
|
||||||
|
defaultValue: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Composite Example
|
||||||
|
|
||||||
|
A single plugin that provides **all three surfaces at once** by combining three of
|
||||||
|
the standalone example plugins:
|
||||||
|
|
||||||
|
| Surface | Source example | File |
|
||||||
|
|---------|----------------|------|
|
||||||
|
| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |
|
||||||
|
| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |
|
||||||
|
| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |
|
||||||
|
|
||||||
|
It demonstrates the `components` manifest map, where each surface points at its own
|
||||||
|
QML file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"type": "composite",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./CompositeDaemon.qml",
|
||||||
|
"widget": "./CompositeBarWidget.qml",
|
||||||
|
"desktop": "./CompositeDesktopWidget.qml"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings
|
||||||
|
namespace (`exampleComposite`), so `pluginData` is the same for every surface.
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
|
||||||
|
- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script
|
||||||
|
on change. Also registers an `IpcHandler` (`target: "compositeExample"`) exposing a
|
||||||
|
`runHook` call, so you can trigger the hook over IPC.
|
||||||
|
- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout
|
||||||
|
that copies to the clipboard.
|
||||||
|
- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Copy this directory into `$CONFIGPATH/DankMaterialShell/plugins/`.
|
||||||
|
2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.
|
||||||
|
(Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they
|
||||||
|
do not auto-load, because they also carry a daemon.)
|
||||||
|
3. Add the bar widget via Settings → Appearance → DankBar Layout.
|
||||||
|
4. Place the desktop clock via Settings → Desktop Widgets.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The daemon surface is instantiated once and lives for as long as the plugin is
|
||||||
|
enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.
|
||||||
|
- Cross-surface runtime state (not needed here) is best shared via
|
||||||
|
`PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each
|
||||||
|
surface is a separate object.
|
||||||
|
- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only
|
||||||
|
understood by DMS 1.5.0 and later.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"id": "exampleComposite",
|
||||||
|
"name": "Composite Example",
|
||||||
|
"description": "One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "DankMaterialShell",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget", "clipboard"],
|
||||||
|
"icon": "extension",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./CompositeDaemon.qml",
|
||||||
|
"widget": "./CompositeBarWidget.qml",
|
||||||
|
"desktop": "./CompositeDesktopWidget.qml"
|
||||||
|
},
|
||||||
|
"settings": "./CompositeSettings.qml",
|
||||||
|
"requires_dms": ">=1.5.0",
|
||||||
|
"permissions": [
|
||||||
|
"settings_read",
|
||||||
|
"settings_write"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1635,6 +1635,79 @@ See `PLUGINS/ExampleDesktopClock/` for a complete working example demonstrating:
|
|||||||
- Responsive sizing
|
- Responsive sizing
|
||||||
- Edit mode handling
|
- Edit mode handling
|
||||||
|
|
||||||
|
## Composite Plugins
|
||||||
|
|
||||||
|
A single plugin can provide **multiple surfaces at once** — for example a background
|
||||||
|
daemon (for IPC / monitoring), a bar widget, and a desktop widget. Because each surface
|
||||||
|
has a different lifecycle (the daemon is instantiated once; bar and desktop widgets are
|
||||||
|
instantiated per bar/placement per screen), each surface is its own QML file.
|
||||||
|
|
||||||
|
### Plugin Type Configuration
|
||||||
|
|
||||||
|
Instead of a single `type` + `component`, declare a `components` map. Set `type` to
|
||||||
|
`composite` (any value works; `composite` is conventional):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myComposite",
|
||||||
|
"name": "My Composite Plugin",
|
||||||
|
"description": "A daemon plus a bar widget plus a desktop widget",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
|
||||||
|
"components": {
|
||||||
|
"daemon": "./MyDaemon.qml",
|
||||||
|
"widget": "./MyBarWidget.qml",
|
||||||
|
"desktop": "./MyDesktopWidget.qml",
|
||||||
|
"launcher": "./MyLauncher.qml"
|
||||||
|
},
|
||||||
|
"trigger": "#",
|
||||||
|
"settings": "./MySettings.qml",
|
||||||
|
"requires_dms": ">=1.5.0",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Surfaces
|
||||||
|
|
||||||
|
Provide any subset of these keys in `components`:
|
||||||
|
|
||||||
|
| Surface | Component contract | Notes |
|
||||||
|
|---------|--------------------|-------|
|
||||||
|
| `widget` | `PluginComponent` (bar pills + optional Control Center widget) | see [Widget Component](#widget-component) |
|
||||||
|
| `desktop` | `DesktopPluginComponent` (or an `Item` following the desktop contract) | see [Desktop Plugins](#desktop-plugins) |
|
||||||
|
| `daemon` | any `Item` exposing `pluginService` / `pluginId` | instantiated once; ideal for IPC handlers and background monitoring |
|
||||||
|
| `launcher` | launcher contract (`getItems` / `executeItem`) | requires `trigger` (or empty-trigger mode); see [Launcher Plugins](#launcher-plugins) |
|
||||||
|
|
||||||
|
Each surface is loaded independently into its own registry, so the same plugin can show
|
||||||
|
up in the bar **and** on the desktop **and** run a daemon simultaneously.
|
||||||
|
|
||||||
|
### Shared State
|
||||||
|
|
||||||
|
Each surface is a separate object, so share runtime state through:
|
||||||
|
|
||||||
|
- `PluginService.getGlobalVar(pluginId, name, default)` / `setGlobalVar(...)` — reactive,
|
||||||
|
in-process, namespaced per plugin (see [Plugin Global Variables](#plugin-global-variables)).
|
||||||
|
- The daemon instance — register `IpcHandler`s or expose data other surfaces read via
|
||||||
|
global vars.
|
||||||
|
- `savePluginData` / `loadPluginData` for persisted settings (all surfaces of a plugin
|
||||||
|
share one settings namespace, so one `settings` component configures them all).
|
||||||
|
|
||||||
|
### Settings, Enabling, and Backwards Compatibility
|
||||||
|
|
||||||
|
- Declare a single top-level `settings` component; it configures every surface.
|
||||||
|
- Composite plugins respect the **enable toggle** in Settings → Plugins (they are not
|
||||||
|
auto-loaded). A pure `desktop` plugin still auto-loads for backwards compatibility.
|
||||||
|
- The legacy single `type` + `component` form is unchanged and fully supported — it is
|
||||||
|
treated internally as a one-entry `components` map.
|
||||||
|
|
||||||
|
### Example Plugin
|
||||||
|
|
||||||
|
See `PLUGINS/ExampleCompositePlugin/` for a working composite that combines the
|
||||||
|
WallpaperWatcher daemon, the Emoji Cycler bar widget, and the Desktop Clock into one
|
||||||
|
plugin.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation
|
- **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation
|
||||||
@@ -1644,6 +1717,7 @@ See `PLUGINS/ExampleDesktopClock/` for a complete working example demonstrating:
|
|||||||
- [LauncherExample](./LauncherExample/)
|
- [LauncherExample](./LauncherExample/)
|
||||||
- [Calculator](https://github.com/rochacbruno/DankCalculator)
|
- [Calculator](https://github.com/rochacbruno/DankCalculator)
|
||||||
- [Desktop Clock](./ExampleDesktopClock/)
|
- [Desktop Clock](./ExampleDesktopClock/)
|
||||||
|
- [Composite Example](./ExampleCompositePlugin/)
|
||||||
- **PluginService**: `Services/PluginService.qml`
|
- **PluginService**: `Services/PluginService.qml`
|
||||||
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
- **Settings UI**: `Modules/Settings/PluginsTab.qml`
|
||||||
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
- **DankBar Integration**: `Modules/DankBar/DankBar.qml`
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
"version",
|
"version",
|
||||||
"author",
|
"author",
|
||||||
"type",
|
"type",
|
||||||
"capabilities",
|
"capabilities"
|
||||||
"component"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -42,8 +41,8 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plugin type",
|
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
|
||||||
"enum": ["widget", "daemon", "launcher", "desktop"]
|
"enum": ["widget", "daemon", "launcher", "desktop", "composite"]
|
||||||
},
|
},
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -55,9 +54,37 @@
|
|||||||
},
|
},
|
||||||
"component": {
|
"component": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relative path to main QML component file",
|
"description": "Relative path to main QML component file. Required unless 'components' is provided.",
|
||||||
"pattern": "^\\./.*\\.qml$"
|
"pattern": "^\\./.*\\.qml$"
|
||||||
},
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.",
|
||||||
|
"properties": {
|
||||||
|
"widget": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Bar/Control Center widget component (PluginComponent)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"desktop": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Desktop widget component",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"daemon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Background daemon component (instantiated once)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Launcher provider component (requires 'trigger')",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1
|
||||||
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
"description": "Trigger string for launcher activation (required for launcher type)"
|
||||||
@@ -109,6 +136,29 @@
|
|||||||
"then": {
|
"then": {
|
||||||
"required": ["trigger"]
|
"required": ["trigger"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["components"],
|
||||||
|
"properties": {
|
||||||
|
"components": {
|
||||||
|
"required": ["launcher"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["trigger"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": ["component"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": ["components"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ Singleton {
|
|||||||
Connections {
|
Connections {
|
||||||
target: PluginService
|
target: PluginService
|
||||||
function onPluginLoaded(pluginId) {
|
function onPluginLoaded(pluginId) {
|
||||||
const plugin = PluginService.availablePlugins[pluginId];
|
if (PluginService.pluginDesktopComponents[pluginId] !== undefined)
|
||||||
if (plugin?.type === "desktop")
|
|
||||||
syncPluginWidgets();
|
syncPluginWidgets();
|
||||||
}
|
}
|
||||||
function onPluginUnloaded(pluginId) {
|
function onPluginUnloaded(pluginId) {
|
||||||
|
|||||||
@@ -21,12 +21,18 @@ Singleton {
|
|||||||
readonly property bool available: socketPath.length > 0
|
readonly property bool available: socketPath.length > 0
|
||||||
|
|
||||||
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
|
||||||
|
readonly property string configPath: configDir + "/mango/config.conf"
|
||||||
readonly property string mangoDmsDir: configDir + "/mango/dms"
|
readonly property string mangoDmsDir: configDir + "/mango/dms"
|
||||||
|
readonly property string bindsPath: mangoDmsDir + "/binds.conf"
|
||||||
|
readonly property string colorsPath: mangoDmsDir + "/colors.conf"
|
||||||
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
||||||
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
||||||
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
||||||
|
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
||||||
|
|
||||||
property int _lastGapValue: -1
|
property int _lastGapValue: -1
|
||||||
|
property real _ignoreWatchedReloadUntil: 0
|
||||||
|
property real _lastWatchedReloadAt: 0
|
||||||
|
|
||||||
// name -> { name, active, x, y, width, height, scale, layoutIndex,
|
// name -> { name, active, x, y, width, height, scale, layoutIndex,
|
||||||
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
|
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
|
||||||
@@ -47,6 +53,55 @@ Singleton {
|
|||||||
// One connection per watch target; mango streams a fresh full snapshot on
|
// One connection per watch target; mango streams a fresh full snapshot on
|
||||||
// every change, so each line is treated as the complete state.
|
// every change, so each line is treated as the complete state.
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoConfigWatcher
|
||||||
|
path: CompositorService.isMango ? root.configPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoBindsWatcher
|
||||||
|
path: CompositorService.isMango ? root.bindsPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoColorsWatcher
|
||||||
|
path: CompositorService.isMango ? root.colorsPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoLayoutWatcher
|
||||||
|
path: CompositorService.isMango ? root.layoutPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoCursorWatcher
|
||||||
|
path: CompositorService.isMango ? root.cursorPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoOutputsWatcher
|
||||||
|
path: CompositorService.isMango ? root.outputsPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: mangoWindowRulesWatcher
|
||||||
|
path: CompositorService.isMango ? root.windowRulesPath : ""
|
||||||
|
watchChanges: CompositorService.isMango
|
||||||
|
onFileChanged: root.handleWatchedConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
DankSocket {
|
DankSocket {
|
||||||
id: monitorsSocket
|
id: monitorsSocket
|
||||||
path: root.socketPath
|
path: root.socketPath
|
||||||
@@ -100,12 +155,14 @@ Singleton {
|
|||||||
for (const m of monitors) {
|
for (const m of monitors) {
|
||||||
if (!m.name)
|
if (!m.name)
|
||||||
continue;
|
continue;
|
||||||
|
const activeTags = m.active_tags || [];
|
||||||
|
const inOverview = activeTags.length === 0 || activeTags.every(t => t === 0);
|
||||||
const tags = (m.tags || []).map(t => ({
|
const tags = (m.tags || []).map(t => ({
|
||||||
// 0-based to match the legacy dwl tag model used by consumers
|
// 0-based to match the legacy dwl tag model used by consumers
|
||||||
"tag": (t.index ?? 1) - 1,
|
"tag": (t.index ?? 1) - 1,
|
||||||
"state": t.is_urgent ? 2 : (t.is_active ? 1 : 0),
|
"state": t.is_urgent ? 2 : (!inOverview && t.is_active ? 1 : 0),
|
||||||
"clients": t.client_count ?? 0,
|
"clients": t.client_count ?? 0,
|
||||||
"focused": !!t.is_active,
|
"focused": !inOverview && !!t.is_active,
|
||||||
"urgent": !!t.is_urgent,
|
"urgent": !!t.is_urgent,
|
||||||
"layout": t.layout ?? ""
|
"layout": t.layout ?? ""
|
||||||
}));
|
}));
|
||||||
@@ -119,7 +176,8 @@ Singleton {
|
|||||||
"scale": m.scale ?? 1.0,
|
"scale": m.scale ?? 1.0,
|
||||||
"layoutIndex": m.layout_index ?? 0,
|
"layoutIndex": m.layout_index ?? 0,
|
||||||
"layout": m.layout_index ?? 0,
|
"layout": m.layout_index ?? 0,
|
||||||
"activeTags": m.active_tags || [],
|
"activeTags": activeTags,
|
||||||
|
"inOverview": inOverview,
|
||||||
"layoutSymbol": m.layout_symbol ?? "",
|
"layoutSymbol": m.layout_symbol ?? "",
|
||||||
"lastOpenSurface": m.last_open_surface ?? "",
|
"lastOpenSurface": m.last_open_surface ?? "",
|
||||||
"keymode": m.keymode ?? "",
|
"keymode": m.keymode ?? "",
|
||||||
@@ -179,6 +237,8 @@ Singleton {
|
|||||||
const output = getOutputState(outputName);
|
const output = getOutputState(outputName);
|
||||||
if (!output)
|
if (!output)
|
||||||
return false;
|
return false;
|
||||||
|
if (output.inOverview !== undefined)
|
||||||
|
return output.inOverview;
|
||||||
const at = output.activeTags || [];
|
const at = output.activeTags || [];
|
||||||
return at.length === 0 || at.every(t => t === 0);
|
return at.length === 0 || at.every(t => t === 0);
|
||||||
}
|
}
|
||||||
@@ -201,6 +261,8 @@ Singleton {
|
|||||||
const output = getOutputState(outputName);
|
const output = getOutputState(outputName);
|
||||||
if (!output || !output.tags)
|
if (!output || !output.tags)
|
||||||
return [];
|
return [];
|
||||||
|
if (isOutputInOverview(outputName))
|
||||||
|
return [];
|
||||||
const visibleTags = new Set();
|
const visibleTags = new Set();
|
||||||
output.tags.forEach(tag => {
|
output.tags.forEach(tag => {
|
||||||
if (tag.state === 1 || tag.clients > 0)
|
if (tag.state === 1 || tag.clients > 0)
|
||||||
@@ -336,10 +398,36 @@ Singleton {
|
|||||||
|
|
||||||
// ── Commands (mango verb IPC: mmsg dispatch <func>,<args>) ─────────────
|
// ── Commands (mango verb IPC: mmsg dispatch <func>,<args>) ─────────────
|
||||||
|
|
||||||
function reloadConfig() {
|
function suppressWatchedConfigReloads(ms) {
|
||||||
|
root._ignoreWatchedReloadUntil = Math.max(root._ignoreWatchedReloadUntil, Date.now() + (ms || 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWatchedConfigChanged() {
|
||||||
|
if (!CompositorService.isMango || !root.available)
|
||||||
|
return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now < root._ignoreWatchedReloadUntil)
|
||||||
|
return;
|
||||||
|
if (now - root._lastWatchedReloadAt < 700)
|
||||||
|
return;
|
||||||
|
root._lastWatchedReloadAt = now;
|
||||||
|
root.reloadConfig(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadConfig(showToast, suppressWatch) {
|
||||||
|
const shouldShowToast = showToast !== false;
|
||||||
|
const shouldSuppressWatch = suppressWatch !== false;
|
||||||
|
if (shouldSuppressWatch)
|
||||||
|
suppressWatchedConfigReloads(1500);
|
||||||
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
|
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0) {
|
||||||
log.warn("mmsg reload_config failed:", output);
|
log.warn("mmsg reload_config failed:", output);
|
||||||
|
if (shouldShowToast)
|
||||||
|
ToastService.showError(I18n.tr("mango: failed to reload config"), output || "", "", "mango-config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldShowToast)
|
||||||
|
ToastService.showInfo(I18n.tr("mango: config reloaded"), "", "", "mango-config");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,17 +626,10 @@ borderpx=${borderSize}
|
|||||||
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
|
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
|
||||||
const size = settings.size || 24;
|
const size = settings.size || 24;
|
||||||
const hideTimeout = settings.mango?.cursorHideTimeout || 0;
|
const hideTimeout = settings.mango?.cursorHideTimeout || 0;
|
||||||
|
const naturalScrolling = SettingsData.mangoTrackpadNaturalScrolling ? 1 : 0;
|
||||||
const isDefaultConfig = !themeName && size === 24 && hideTimeout === 0;
|
|
||||||
if (isDefaultConfig) {
|
|
||||||
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
|
|
||||||
if (exitCode !== 0)
|
|
||||||
log.warn("Failed to write cursor config:", output);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = `# Auto-generated by DMS - do not edit manually
|
let content = `# Auto-generated by DMS - do not edit manually
|
||||||
|
trackpad_natural_scrolling=${naturalScrolling}
|
||||||
cursor_size=${size}`;
|
cursor_size=${size}`;
|
||||||
|
|
||||||
if (themeName)
|
if (themeName)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtCore
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.folderlistmodel
|
import Qt.labs.folderlistmodel
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -202,8 +201,51 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property var pluginSurfaceKeys: ["widget", "desktop", "daemon", "launcher"]
|
||||||
|
|
||||||
|
function _stripDotSlash(p) {
|
||||||
|
return p.startsWith("./") ? p.slice(2) : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _deriveLegacySurface(type, capabilities) {
|
||||||
|
if (type === "daemon")
|
||||||
|
return "daemon";
|
||||||
|
if (type === "launcher" || (capabilities && capabilities.includes("launcher")))
|
||||||
|
return "launcher";
|
||||||
|
if (type === "desktop")
|
||||||
|
return "desktop";
|
||||||
|
return "widget";
|
||||||
|
}
|
||||||
|
|
||||||
|
function _resolveComponentPaths(manifest, dir) {
|
||||||
|
const paths = {};
|
||||||
|
if (manifest.components && typeof manifest.components === "object") {
|
||||||
|
for (const surface in manifest.components) {
|
||||||
|
if (!pluginSurfaceKeys.includes(surface)) {
|
||||||
|
log.warn("unknown plugin surface", surface, "in", dir);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = manifest.components[surface];
|
||||||
|
if (!rel)
|
||||||
|
continue;
|
||||||
|
paths[surface] = dir + "/" + _stripDotSlash(rel);
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
if (manifest.component) {
|
||||||
|
const surface = _deriveLegacySurface(manifest.type, manifest.capabilities);
|
||||||
|
paths[surface] = dir + "/" + _stripDotSlash(manifest.component);
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluginHasSurface(pluginId, surface) {
|
||||||
|
const plugin = availablePlugins[pluginId];
|
||||||
|
return !!(plugin && plugin.surfaces && plugin.surfaces.includes(surface));
|
||||||
|
}
|
||||||
|
|
||||||
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
|
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
|
||||||
if (!manifest || !manifest.id || !manifest.name || !manifest.component) {
|
if (!manifest || !manifest.id || !manifest.name || (!manifest.component && !manifest.components)) {
|
||||||
log.error("invalid manifest fields:", absPath);
|
log.error("invalid manifest fields:", absPath);
|
||||||
knownManifests[absPath] = {
|
knownManifests[absPath] = {
|
||||||
mtime: mtimeEpochMs,
|
mtime: mtimeEpochMs,
|
||||||
@@ -214,13 +256,22 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dir = absPath.substring(0, absPath.lastIndexOf('/'));
|
const dir = absPath.substring(0, absPath.lastIndexOf('/'));
|
||||||
let comp = manifest.component;
|
|
||||||
if (comp.startsWith("./"))
|
|
||||||
comp = comp.slice(2);
|
|
||||||
let settings = manifest.settings;
|
let settings = manifest.settings;
|
||||||
if (settings && settings.startsWith("./"))
|
if (settings && settings.startsWith("./"))
|
||||||
settings = settings.slice(2);
|
settings = settings.slice(2);
|
||||||
|
|
||||||
|
const componentPaths = _resolveComponentPaths(manifest, dir);
|
||||||
|
const surfaces = Object.keys(componentPaths);
|
||||||
|
if (surfaces.length === 0) {
|
||||||
|
log.error("no valid component surfaces in manifest:", absPath);
|
||||||
|
knownManifests[absPath] = {
|
||||||
|
mtime: mtimeEpochMs,
|
||||||
|
source: sourceTag,
|
||||||
|
bad: true
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const info = {};
|
const info = {};
|
||||||
for (const k in manifest)
|
for (const k in manifest)
|
||||||
info[k] = manifest[k];
|
info[k] = manifest[k];
|
||||||
@@ -236,10 +287,12 @@ Singleton {
|
|||||||
|
|
||||||
info.manifestPath = absPath;
|
info.manifestPath = absPath;
|
||||||
info.pluginDirectory = dir;
|
info.pluginDirectory = dir;
|
||||||
info.componentPath = dir + "/" + comp;
|
info.componentPaths = componentPaths;
|
||||||
|
info.surfaces = surfaces;
|
||||||
|
info.componentPath = componentPaths.widget || componentPaths[surfaces[0]];
|
||||||
info.settingsPath = settings ? (dir + "/" + settings) : null;
|
info.settingsPath = settings ? (dir + "/" + settings) : null;
|
||||||
info.loaded = isPluginLoaded(manifest.id);
|
info.loaded = isPluginLoaded(manifest.id);
|
||||||
info.type = manifest.type || "widget";
|
info.type = manifest.type || (manifest.components ? "composite" : "widget");
|
||||||
info.source = sourceTag;
|
info.source = sourceTag;
|
||||||
info.requires_dms = manifest.requires_dms || null;
|
info.requires_dms = manifest.requires_dms || null;
|
||||||
|
|
||||||
@@ -260,7 +313,8 @@ Singleton {
|
|||||||
};
|
};
|
||||||
_updateAvailablePluginsList();
|
_updateAvailablePluginsList();
|
||||||
pluginListUpdated();
|
pluginListUpdated();
|
||||||
const enabled = info.type === "desktop" || SettingsData.getPluginSetting(manifest.id, "enabled", false);
|
const isPureDesktop = surfaces.length === 1 && surfaces[0] === "desktop";
|
||||||
|
const enabled = isPureDesktop || SettingsData.getPluginSetting(manifest.id, "enabled", false);
|
||||||
if (enabled && !info.loaded)
|
if (enabled && !info.loaded)
|
||||||
loadPlugin(manifest.id);
|
loadPlugin(manifest.id);
|
||||||
} else {
|
} else {
|
||||||
@@ -296,58 +350,69 @@ Singleton {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDaemon = plugin.type === "daemon";
|
const componentPaths = plugin.componentPaths || {};
|
||||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
|
const surfaces = Object.keys(componentPaths);
|
||||||
const isDesktop = plugin.type === "desktop";
|
if (surfaces.length === 0) {
|
||||||
|
log.error("Plugin has no component surfaces:", pluginId);
|
||||||
|
pluginLoadFailed(pluginId, "No component surfaces");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const prevInstance = pluginInstances[pluginId];
|
const newWidgets = Object.assign({}, pluginWidgetComponents);
|
||||||
|
const newDesktop = Object.assign({}, pluginDesktopComponents);
|
||||||
|
const newDaemons = Object.assign({}, pluginDaemonComponents);
|
||||||
|
const newLaunchers = Object.assign({}, pluginLauncherComponents);
|
||||||
|
const newInstances = Object.assign({}, pluginInstances);
|
||||||
|
|
||||||
|
const prevInstance = newInstances[pluginId];
|
||||||
if (prevInstance) {
|
if (prevInstance) {
|
||||||
prevInstance.destroy();
|
prevInstance.destroy();
|
||||||
const newInstances = Object.assign({}, pluginInstances);
|
|
||||||
delete newInstances[pluginId];
|
delete newInstances[pluginId];
|
||||||
pluginInstances = newInstances;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let url = "file://" + plugin.componentPath;
|
for (const surface of surfaces) {
|
||||||
|
let url = "file://" + componentPaths[surface];
|
||||||
if (bustCache)
|
if (bustCache)
|
||||||
url += "?t=" + Date.now();
|
url += "?t=" + Date.now();
|
||||||
const comp = Qt.createComponent(url, Component.PreferSynchronous);
|
const comp = Qt.createComponent(url, Component.PreferSynchronous);
|
||||||
if (comp.status === Component.Error) {
|
if (comp.status === Component.Error) {
|
||||||
log.error("component error", pluginId, comp.errorString());
|
log.error("component error", pluginId, surface, comp.errorString());
|
||||||
pluginLoadFailed(pluginId, comp.errorString());
|
pluginLoadFailed(pluginId, comp.errorString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDaemon) {
|
switch (surface) {
|
||||||
const newDaemons = Object.assign({}, pluginDaemonComponents);
|
case "daemon":
|
||||||
newDaemons[pluginId] = comp;
|
newDaemons[pluginId] = comp;
|
||||||
pluginDaemonComponents = newDaemons;
|
break;
|
||||||
} else if (isLauncher) {
|
case "desktop":
|
||||||
|
newDesktop[pluginId] = comp;
|
||||||
|
break;
|
||||||
|
case "launcher": {
|
||||||
const instance = comp.createObject(root, {
|
const instance = comp.createObject(root, {
|
||||||
"pluginService": root
|
"pluginService": root
|
||||||
});
|
});
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
log.error("failed to instantiate plugin:", pluginId, comp.errorString());
|
log.error("failed to instantiate launcher surface:", pluginId, comp.errorString());
|
||||||
pluginLoadFailed(pluginId, comp.errorString());
|
pluginLoadFailed(pluginId, comp.errorString());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const newInstances = Object.assign({}, pluginInstances);
|
|
||||||
newInstances[pluginId] = instance;
|
newInstances[pluginId] = instance;
|
||||||
pluginInstances = newInstances;
|
|
||||||
|
|
||||||
const newLaunchers = Object.assign({}, pluginLauncherComponents);
|
|
||||||
newLaunchers[pluginId] = comp;
|
newLaunchers[pluginId] = comp;
|
||||||
pluginLauncherComponents = newLaunchers;
|
break;
|
||||||
} else if (isDesktop) {
|
|
||||||
const newDesktop = Object.assign({}, pluginDesktopComponents);
|
|
||||||
newDesktop[pluginId] = comp;
|
|
||||||
pluginDesktopComponents = newDesktop;
|
|
||||||
} else {
|
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents);
|
|
||||||
newComponents[pluginId] = comp;
|
|
||||||
pluginWidgetComponents = newComponents;
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
newWidgets[pluginId] = comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginWidgetComponents = newWidgets;
|
||||||
|
pluginDesktopComponents = newDesktop;
|
||||||
|
pluginDaemonComponents = newDaemons;
|
||||||
|
pluginLauncherComponents = newLaunchers;
|
||||||
|
pluginInstances = newInstances;
|
||||||
|
|
||||||
plugin.loaded = true;
|
plugin.loaded = true;
|
||||||
const newLoaded = Object.assign({}, loadedPlugins);
|
const newLoaded = Object.assign({}, loadedPlugins);
|
||||||
@@ -371,10 +436,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isDaemon = plugin.type === "daemon";
|
|
||||||
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
|
|
||||||
const isDesktop = plugin.type === "desktop";
|
|
||||||
|
|
||||||
const instance = pluginInstances[pluginId];
|
const instance = pluginInstances[pluginId];
|
||||||
if (instance) {
|
if (instance) {
|
||||||
instance.destroy();
|
instance.destroy();
|
||||||
@@ -383,19 +444,22 @@ Singleton {
|
|||||||
pluginInstances = newInstances;
|
pluginInstances = newInstances;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDaemon && pluginDaemonComponents[pluginId]) {
|
if (pluginDaemonComponents[pluginId]) {
|
||||||
const newDaemons = Object.assign({}, pluginDaemonComponents);
|
const newDaemons = Object.assign({}, pluginDaemonComponents);
|
||||||
delete newDaemons[pluginId];
|
delete newDaemons[pluginId];
|
||||||
pluginDaemonComponents = newDaemons;
|
pluginDaemonComponents = newDaemons;
|
||||||
} else if (isLauncher && pluginLauncherComponents[pluginId]) {
|
}
|
||||||
|
if (pluginLauncherComponents[pluginId]) {
|
||||||
const newLaunchers = Object.assign({}, pluginLauncherComponents);
|
const newLaunchers = Object.assign({}, pluginLauncherComponents);
|
||||||
delete newLaunchers[pluginId];
|
delete newLaunchers[pluginId];
|
||||||
pluginLauncherComponents = newLaunchers;
|
pluginLauncherComponents = newLaunchers;
|
||||||
} else if (isDesktop && pluginDesktopComponents[pluginId]) {
|
}
|
||||||
|
if (pluginDesktopComponents[pluginId]) {
|
||||||
const newDesktop = Object.assign({}, pluginDesktopComponents);
|
const newDesktop = Object.assign({}, pluginDesktopComponents);
|
||||||
delete newDesktop[pluginId];
|
delete newDesktop[pluginId];
|
||||||
pluginDesktopComponents = newDesktop;
|
pluginDesktopComponents = newDesktop;
|
||||||
} else if (pluginWidgetComponents[pluginId]) {
|
}
|
||||||
|
if (pluginWidgetComponents[pluginId]) {
|
||||||
const newComponents = Object.assign({}, pluginWidgetComponents);
|
const newComponents = Object.assign({}, pluginWidgetComponents);
|
||||||
delete newComponents[pluginId];
|
delete newComponents[pluginId];
|
||||||
pluginWidgetComponents = newComponents;
|
pluginWidgetComponents = newComponents;
|
||||||
@@ -452,7 +516,8 @@ Singleton {
|
|||||||
const result = [];
|
const result = [];
|
||||||
for (const pluginId in availablePlugins) {
|
for (const pluginId in availablePlugins) {
|
||||||
const plugin = availablePlugins[pluginId];
|
const plugin = availablePlugins[pluginId];
|
||||||
if (plugin.type !== "widget") {
|
const hasWidgetSurface = plugin.surfaces ? plugin.surfaces.includes("widget") : (plugin.type === "widget");
|
||||||
|
if (!hasWidgetSurface) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const variants = getPluginVariants(pluginId);
|
const variants = getPluginVariants(pluginId);
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ Singleton {
|
|||||||
var item = settingsIndex[i];
|
var item = settingsIndex[i];
|
||||||
var t = translateItem(item);
|
var t = translateItem(item);
|
||||||
var sourceDescription = item.description || "";
|
var sourceDescription = item.description || "";
|
||||||
|
var labelLower = _lowerVariants([item.label, t.label]);
|
||||||
|
var categoryLower = _lowerVariants([item.category, t.category]);
|
||||||
|
var descriptionLower = _lowerVariants([sourceDescription, t.description]);
|
||||||
cache.push({
|
cache.push({
|
||||||
section: t.section,
|
section: t.section,
|
||||||
label: t.label,
|
label: t.label,
|
||||||
@@ -166,9 +169,14 @@ Singleton {
|
|||||||
icon: t.icon,
|
icon: t.icon,
|
||||||
description: t.description,
|
description: t.description,
|
||||||
conditionKey: t.conditionKey,
|
conditionKey: t.conditionKey,
|
||||||
labelSearch: _lowerVariants([item.label, t.label]),
|
isTab: String(t.section).startsWith("_tab_"),
|
||||||
categorySearch: _lowerVariants([item.category, t.category]),
|
labelSearch: labelLower,
|
||||||
descriptionSearch: _lowerVariants([sourceDescription, t.description])
|
categorySearch: categoryLower,
|
||||||
|
descriptionSearch: descriptionLower,
|
||||||
|
labelSquash: _squashVariants(labelLower),
|
||||||
|
categorySquash: _squashVariants(categoryLower),
|
||||||
|
descriptionSquash: _squashVariants(descriptionLower),
|
||||||
|
keywordsSquash: _squashVariants(t.keywords)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_translatedCache = cache;
|
_translatedCache = cache;
|
||||||
@@ -187,6 +195,22 @@ Singleton {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _squash(value) {
|
||||||
|
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _squashVariants(values) {
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
if (!values[i])
|
||||||
|
continue;
|
||||||
|
var squashed = _squash(values[i]);
|
||||||
|
if (squashed && out.indexOf(squashed) === -1)
|
||||||
|
out.push(squashed);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function _bestFieldScore(fields, queryLower, exactScore, prefixScore, includesScore) {
|
function _bestFieldScore(fields, queryLower, exactScore, prefixScore, includesScore) {
|
||||||
var score = 0;
|
var score = 0;
|
||||||
for (var i = 0; i < fields.length; i++) {
|
for (var i = 0; i < fields.length; i++) {
|
||||||
@@ -223,6 +247,7 @@ Singleton {
|
|||||||
return [];
|
return [];
|
||||||
|
|
||||||
var queryLower = text.toLowerCase().trim();
|
var queryLower = text.toLowerCase().trim();
|
||||||
|
var querySquash = _squash(queryLower);
|
||||||
var queryWords = queryLower.split(/\s+/).filter(w => w.length > 0);
|
var queryWords = queryLower.split(/\s+/).filter(w => w.length > 0);
|
||||||
var scored = [];
|
var scored = [];
|
||||||
var cache = _translatedCache;
|
var cache = _translatedCache;
|
||||||
@@ -233,25 +258,47 @@ Singleton {
|
|||||||
if (!checkCondition(entry))
|
if (!checkCondition(entry))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var score = 0;
|
var labelScore = _bestFieldScore(entry.labelSearch, queryLower, 10000, 5000, 1000);
|
||||||
|
if (querySquash)
|
||||||
|
labelScore = Math.max(labelScore, _bestFieldScore(entry.labelSquash, querySquash, 9000, 4500, 900));
|
||||||
|
|
||||||
score = Math.max(score, _bestFieldScore(entry.labelSearch, queryLower, 10000, 5000, 1000));
|
var score = labelScore;
|
||||||
score = Math.max(score, _bestFieldScore(entry.categorySearch, queryLower, 500, 500, 500));
|
score = Math.max(score, _bestFieldScore(entry.categorySearch, queryLower, 500, 500, 500));
|
||||||
score = Math.max(score, _bestFieldScore(entry.descriptionSearch, queryLower, 250, 250, 250));
|
score = Math.max(score, _bestFieldScore(entry.descriptionSearch, queryLower, 250, 250, 250));
|
||||||
|
if (querySquash) {
|
||||||
|
score = Math.max(score, _bestFieldScore(entry.categorySquash, querySquash, 500, 500, 500));
|
||||||
|
score = Math.max(score, _bestFieldScore(entry.descriptionSquash, querySquash, 250, 250, 250));
|
||||||
|
}
|
||||||
|
|
||||||
if (score === 0) {
|
if (score === 0) {
|
||||||
var keywords = entry.keywords;
|
var keywords = entry.keywords;
|
||||||
for (var k = 0; k < keywords.length; k++) {
|
for (var k = 0; k < keywords.length; k++) {
|
||||||
if (keywords[k].startsWith(queryLower)) {
|
var keyword = keywords[k];
|
||||||
score = 800;
|
if (keyword === queryLower) {
|
||||||
|
score = 900;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (keywords[k].includes(queryLower) && score < 400) {
|
if (keyword.startsWith(queryLower)) {
|
||||||
|
score = Math.max(score, 800);
|
||||||
|
} else if (keyword.includes(queryLower) && score < 400) {
|
||||||
score = 400;
|
score = 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (score === 0 && querySquash) {
|
||||||
|
var keywordsSquash = entry.keywordsSquash;
|
||||||
|
for (var ks = 0; ks < keywordsSquash.length; ks++) {
|
||||||
|
if (keywordsSquash[ks] === querySquash) {
|
||||||
|
score = Math.max(score, 850);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (keywordsSquash[ks].startsWith(querySquash)) {
|
||||||
|
score = Math.max(score, 750);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (score === 0 && queryWords.length > 1) {
|
if (score === 0 && queryWords.length > 1) {
|
||||||
var allMatch = true;
|
var allMatch = true;
|
||||||
for (var w = 0; w < queryWords.length; w++) {
|
for (var w = 0; w < queryWords.length; w++) {
|
||||||
@@ -281,12 +328,21 @@ Singleton {
|
|||||||
if (score > 0) {
|
if (score > 0) {
|
||||||
scored.push({
|
scored.push({
|
||||||
item: entry,
|
item: entry,
|
||||||
score: score
|
score: score,
|
||||||
|
labelScore: labelScore
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scored.sort((a, b) => b.score - a.score);
|
scored.sort((a, b) => {
|
||||||
|
if (b.score !== a.score)
|
||||||
|
return b.score - a.score;
|
||||||
|
if (b.labelScore !== a.labelScore)
|
||||||
|
return b.labelScore - a.labelScore;
|
||||||
|
if (a.item.isTab !== b.item.isTab)
|
||||||
|
return a.item.isTab ? 1 : -1;
|
||||||
|
return a.item.label.length - b.item.label.length;
|
||||||
|
});
|
||||||
return scored.slice(0, limit).map(s => s.item);
|
return scored.slice(0, limit).map(s => s.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ CATEGORY_KEYWORDS = {
|
|||||||
"opacity",
|
"opacity",
|
||||||
],
|
],
|
||||||
"Locale": ["locale", "language", "country"],
|
"Locale": ["locale", "language", "country"],
|
||||||
|
"Greeter": ["login", "greetd", "display manager"],
|
||||||
|
"Multiplexers": ["tmux", "zellij", "terminal"],
|
||||||
|
"Frame": ["window", "border", "decoration"],
|
||||||
|
"Default Apps": ["browser", "terminal", "handlers", "mime"],
|
||||||
|
"Users": ["accounts", "user", "profile"],
|
||||||
|
"Autostart": ["startup", "launch", "boot"],
|
||||||
}
|
}
|
||||||
|
|
||||||
TAB_INDEX_MAP = {
|
TAB_INDEX_MAP = {
|
||||||
@@ -124,6 +130,12 @@ TAB_INDEX_MAP = {
|
|||||||
"WindowRulesTab.qml": 28,
|
"WindowRulesTab.qml": 28,
|
||||||
"AudioTab.qml": 29,
|
"AudioTab.qml": 29,
|
||||||
"LocaleTab.qml": 30,
|
"LocaleTab.qml": 30,
|
||||||
|
"GreeterTab.qml": 31,
|
||||||
|
"MuxTab.qml": 32,
|
||||||
|
"FrameTab.qml": 33,
|
||||||
|
"DefaultAppsTab.qml": 34,
|
||||||
|
"UsersTab.qml": 35,
|
||||||
|
"AutoStartTab.qml": 36,
|
||||||
}
|
}
|
||||||
|
|
||||||
TAB_CATEGORY_MAP = {
|
TAB_CATEGORY_MAP = {
|
||||||
@@ -157,6 +169,12 @@ TAB_CATEGORY_MAP = {
|
|||||||
28: "Window Rules",
|
28: "Window Rules",
|
||||||
29: "Audio",
|
29: "Audio",
|
||||||
30: "Locale",
|
30: "Locale",
|
||||||
|
31: "Greeter",
|
||||||
|
32: "Multiplexers",
|
||||||
|
33: "Frame",
|
||||||
|
34: "Default Apps",
|
||||||
|
35: "Users",
|
||||||
|
36: "Autostart",
|
||||||
}
|
}
|
||||||
|
|
||||||
SEARCHABLE_COMPONENTS = [
|
SEARCHABLE_COMPONENTS = [
|
||||||
|
|||||||
@@ -1051,6 +1051,31 @@
|
|||||||
"description": "Show workspaces of the currently focused monitor",
|
"description": "Show workspaces of the currently focused monitor",
|
||||||
"conditionKey": "isNiri"
|
"conditionKey": "isNiri"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "groupActiveWorkspaceApps",
|
||||||
|
"label": "Group Active Workspace",
|
||||||
|
"tabIndex": 4,
|
||||||
|
"category": "Workspaces",
|
||||||
|
"keywords": [
|
||||||
|
"active",
|
||||||
|
"app",
|
||||||
|
"application",
|
||||||
|
"apps",
|
||||||
|
"desktop",
|
||||||
|
"focused",
|
||||||
|
"group",
|
||||||
|
"grouped",
|
||||||
|
"icons",
|
||||||
|
"program",
|
||||||
|
"repeated",
|
||||||
|
"spaces",
|
||||||
|
"virtual",
|
||||||
|
"virtual desktops",
|
||||||
|
"workspace",
|
||||||
|
"workspaces"
|
||||||
|
],
|
||||||
|
"description": "Also group repeated application icons on the active workspace"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "groupWorkspaceApps",
|
"section": "groupWorkspaceApps",
|
||||||
"label": "Group Workspace Apps",
|
"label": "Group Workspace Apps",
|
||||||
@@ -3901,6 +3926,30 @@
|
|||||||
],
|
],
|
||||||
"description": "Shadow elevation on popouts, OSDs, and dropdowns"
|
"description": "Shadow elevation on popouts, OSDs, and dropdowns"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "hyprlandResizeOnBorder",
|
||||||
|
"label": "Resize on Border",
|
||||||
|
"tabIndex": 10,
|
||||||
|
"category": "Theme & Colors",
|
||||||
|
"keywords": [
|
||||||
|
"appearance",
|
||||||
|
"border",
|
||||||
|
"colors",
|
||||||
|
"drag",
|
||||||
|
"dragging",
|
||||||
|
"edges",
|
||||||
|
"hyprland",
|
||||||
|
"look",
|
||||||
|
"mouse",
|
||||||
|
"resize",
|
||||||
|
"scheme",
|
||||||
|
"style",
|
||||||
|
"their",
|
||||||
|
"theme",
|
||||||
|
"windows"
|
||||||
|
],
|
||||||
|
"description": "Resize windows by dragging their edges with the mouse"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "runDmsMatugenTemplates",
|
"section": "runDmsMatugenTemplates",
|
||||||
"label": "Run DMS Templates",
|
"label": "Run DMS Templates",
|
||||||
@@ -7585,8 +7634,7 @@
|
|||||||
"rules",
|
"rules",
|
||||||
"window"
|
"window"
|
||||||
],
|
],
|
||||||
"icon": "select_window",
|
"icon": "select_window"
|
||||||
"conditionKey": "isNiri"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"section": "audioInputDevices",
|
"section": "audioInputDevices",
|
||||||
@@ -7691,61 +7739,874 @@
|
|||||||
],
|
],
|
||||||
"description": "Change the locale used for date and time formatting, independent of the interface language."
|
"description": "Change the locale used for date and time formatting, independent of the interface language."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterUse24Hour",
|
||||||
|
"label": "24-hour clock",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"24hour",
|
||||||
|
"affect",
|
||||||
|
"clock",
|
||||||
|
"display manager",
|
||||||
|
"does",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"hour",
|
||||||
|
"login",
|
||||||
|
"main",
|
||||||
|
"time",
|
||||||
|
"watch"
|
||||||
|
],
|
||||||
|
"description": "Greeter only — does not affect main clock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterAutoLogin",
|
||||||
|
"label": "Auto-login on startup",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"after",
|
||||||
|
"auto",
|
||||||
|
"autologin",
|
||||||
|
"boot",
|
||||||
|
"display manager",
|
||||||
|
"effect",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"lock",
|
||||||
|
"lockscreen",
|
||||||
|
"login",
|
||||||
|
"next",
|
||||||
|
"password",
|
||||||
|
"reboot",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"sign",
|
||||||
|
"skip",
|
||||||
|
"startup",
|
||||||
|
"sync",
|
||||||
|
"takes",
|
||||||
|
"unchanged",
|
||||||
|
"unlock",
|
||||||
|
"until"
|
||||||
|
],
|
||||||
|
"description": "Skip the greeter password after boot until you sign out. Lock screen unlock is unchanged. Takes effect on the next reboot after sync."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterLockDateFormat",
|
||||||
|
"label": "Date Format",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"date",
|
||||||
|
"display manager",
|
||||||
|
"format",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"screen"
|
||||||
|
],
|
||||||
|
"description": "Greeter only — format for the date on the login screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterDeps",
|
||||||
|
"label": "Dependencies & documentation",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"dependencies",
|
||||||
|
"display manager",
|
||||||
|
"documentation",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login"
|
||||||
|
],
|
||||||
|
"icon": "extension"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterEnableFprint",
|
||||||
|
"label": "Enable fingerprint at login",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"display manager",
|
||||||
|
"enable",
|
||||||
|
"fingerprint",
|
||||||
|
"fprintd",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterEnableU2f",
|
||||||
|
"label": "Enable security key at login",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"display manager",
|
||||||
|
"enable",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"key",
|
||||||
|
"login",
|
||||||
|
"security",
|
||||||
|
"u2f"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "_tab_31",
|
"section": "_tab_31",
|
||||||
"label": "Greeter",
|
"label": "Greeter",
|
||||||
"tabIndex": 31,
|
"tabIndex": 31,
|
||||||
"category": "Settings",
|
"category": "Greeter",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
"greeter",
|
"greeter",
|
||||||
"settings"
|
"login"
|
||||||
],
|
],
|
||||||
"icon": "login"
|
"icon": "login"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterAppearance",
|
||||||
|
"label": "Greeter Appearance",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"appearance",
|
||||||
|
"display manager",
|
||||||
|
"font",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"screen",
|
||||||
|
"typography"
|
||||||
|
],
|
||||||
|
"icon": "palette",
|
||||||
|
"description": "Font used on the login screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterBehavior",
|
||||||
|
"label": "Greeter Behavior",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"behavior",
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"last",
|
||||||
|
"login",
|
||||||
|
"remember",
|
||||||
|
"select",
|
||||||
|
"session"
|
||||||
|
],
|
||||||
|
"icon": "history",
|
||||||
|
"description": "Pre-select the last used session on the greeter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterStatus",
|
||||||
|
"label": "Greeter Status",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"icon": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterFontFamily",
|
||||||
|
"label": "Greeter font",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"font",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"screen",
|
||||||
|
"typography"
|
||||||
|
],
|
||||||
|
"description": "Font used on the login screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterAuth",
|
||||||
|
"label": "Login Authentication",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"authentication",
|
||||||
|
"display manager",
|
||||||
|
"fingerprint",
|
||||||
|
"fprintd",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login"
|
||||||
|
],
|
||||||
|
"icon": "fingerprint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterPadHours",
|
||||||
|
"label": "Pad hours (02:00 vs 2:00)",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"(02:00",
|
||||||
|
"12hour",
|
||||||
|
"2:00)",
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"hours",
|
||||||
|
"login",
|
||||||
|
"pad",
|
||||||
|
"time"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterRememberLastSession",
|
||||||
|
"label": "Remember last session",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"last",
|
||||||
|
"login",
|
||||||
|
"remember",
|
||||||
|
"select",
|
||||||
|
"session"
|
||||||
|
],
|
||||||
|
"description": "Pre-select the last used session on the greeter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterRememberLastUser",
|
||||||
|
"label": "Remember last user",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"fill",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"last",
|
||||||
|
"login",
|
||||||
|
"remember",
|
||||||
|
"successful",
|
||||||
|
"user",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"description": "Pre-fill the last successful username on the greeter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterShowSeconds",
|
||||||
|
"label": "Show Seconds",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"display manager",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"login",
|
||||||
|
"seconds",
|
||||||
|
"show",
|
||||||
|
"time"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "greeterWallpaperFillMode",
|
||||||
|
"label": "Wallpaper fill mode",
|
||||||
|
"tabIndex": 31,
|
||||||
|
"category": "Greeter",
|
||||||
|
"keywords": [
|
||||||
|
"background",
|
||||||
|
"bg",
|
||||||
|
"desktop",
|
||||||
|
"display manager",
|
||||||
|
"fill",
|
||||||
|
"greetd",
|
||||||
|
"greeter",
|
||||||
|
"image",
|
||||||
|
"login",
|
||||||
|
"mode",
|
||||||
|
"picture",
|
||||||
|
"scaled",
|
||||||
|
"wallpaper"
|
||||||
|
],
|
||||||
|
"description": "How the background image is scaled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "muxType",
|
||||||
|
"label": "Multiplexer",
|
||||||
|
"tabIndex": 32,
|
||||||
|
"category": "Multiplexers",
|
||||||
|
"keywords": [
|
||||||
|
"backend",
|
||||||
|
"multiplexer",
|
||||||
|
"multiplexers",
|
||||||
|
"mux",
|
||||||
|
"terminal",
|
||||||
|
"tmux",
|
||||||
|
"type",
|
||||||
|
"zellij"
|
||||||
|
],
|
||||||
|
"icon": "terminal",
|
||||||
|
"description": "Terminal multiplexer backend to use"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "_tab_32",
|
"section": "_tab_32",
|
||||||
"label": "Multiplexers",
|
"label": "Multiplexers",
|
||||||
"tabIndex": 32,
|
"tabIndex": 32,
|
||||||
"category": "Settings",
|
"category": "Multiplexers",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"multiplexers",
|
"multiplexers",
|
||||||
"settings"
|
"terminal",
|
||||||
|
"tmux",
|
||||||
|
"zellij"
|
||||||
],
|
],
|
||||||
"icon": "terminal"
|
"icon": "terminal"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "muxUseCustomCommand",
|
||||||
|
"label": "Terminal",
|
||||||
|
"tabIndex": 32,
|
||||||
|
"category": "Multiplexers",
|
||||||
|
"keywords": [
|
||||||
|
"command",
|
||||||
|
"custom",
|
||||||
|
"multiplexers",
|
||||||
|
"mux",
|
||||||
|
"override",
|
||||||
|
"script",
|
||||||
|
"terminal",
|
||||||
|
"tmux",
|
||||||
|
"zellij"
|
||||||
|
],
|
||||||
|
"icon": "desktop_windows",
|
||||||
|
"description": "Override terminal with a custom command or script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameLauncherArcExtender",
|
||||||
|
"label": "Arc Extender",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"app drawer",
|
||||||
|
"app menu",
|
||||||
|
"applications",
|
||||||
|
"arc",
|
||||||
|
"border",
|
||||||
|
"center",
|
||||||
|
"connected",
|
||||||
|
"content",
|
||||||
|
"decoration",
|
||||||
|
"extended",
|
||||||
|
"extender",
|
||||||
|
"frame",
|
||||||
|
"launcher",
|
||||||
|
"start menu",
|
||||||
|
"surface",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Use the extended surface for launcher content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameBorder",
|
||||||
|
"label": "Border",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"corner",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"horizontal",
|
||||||
|
"panel",
|
||||||
|
"radius",
|
||||||
|
"rounding",
|
||||||
|
"statusbar",
|
||||||
|
"taskbar",
|
||||||
|
"thickness",
|
||||||
|
"topbar",
|
||||||
|
"vertical",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "border_outer",
|
||||||
|
"description": "Horizontal and vertical bar thickness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameColor",
|
||||||
|
"label": "Border Color",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"color",
|
||||||
|
"colour",
|
||||||
|
"decoration",
|
||||||
|
"default",
|
||||||
|
"frame",
|
||||||
|
"hue",
|
||||||
|
"primary",
|
||||||
|
"surface",
|
||||||
|
"theme",
|
||||||
|
"tint",
|
||||||
|
"window"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameRounding",
|
||||||
|
"label": "Border Radius",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"corner",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"radius",
|
||||||
|
"rounding",
|
||||||
|
"window"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameThickness",
|
||||||
|
"label": "Border Width",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"size",
|
||||||
|
"thickness",
|
||||||
|
"width",
|
||||||
|
"window"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameConnectedOptions",
|
||||||
|
"label": "Connected Options",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"arcs",
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"curves",
|
||||||
|
"decoration",
|
||||||
|
"edge",
|
||||||
|
"expose",
|
||||||
|
"frame",
|
||||||
|
"gap",
|
||||||
|
"meet",
|
||||||
|
"notification",
|
||||||
|
"options",
|
||||||
|
"popout",
|
||||||
|
"reveal",
|
||||||
|
"surfaces",
|
||||||
|
"where",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "blur_linear",
|
||||||
|
"description": "Reveal the arcs where surfaces meet the frame"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameDisplays",
|
||||||
|
"label": "Display Assignment",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"assignment",
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"display",
|
||||||
|
"frame",
|
||||||
|
"monitor",
|
||||||
|
"output",
|
||||||
|
"screen",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "monitor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameEnable",
|
||||||
|
"label": "Enable Frame",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"around",
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"decoration",
|
||||||
|
"display",
|
||||||
|
"draw",
|
||||||
|
"enable",
|
||||||
|
"entire",
|
||||||
|
"frame",
|
||||||
|
"monitor",
|
||||||
|
"outline",
|
||||||
|
"output",
|
||||||
|
"picture",
|
||||||
|
"screen",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Draw a connected picture-frame border around the entire display"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameCloseGaps",
|
||||||
|
"label": "Expose the Arcs",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"arcs",
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"curves",
|
||||||
|
"decoration",
|
||||||
|
"edge",
|
||||||
|
"expose",
|
||||||
|
"frame",
|
||||||
|
"gap",
|
||||||
|
"meet",
|
||||||
|
"notification",
|
||||||
|
"popout",
|
||||||
|
"reveal",
|
||||||
|
"surfaces",
|
||||||
|
"where",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Reveal the arcs where surfaces meet the frame"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "_tab_33",
|
"section": "_tab_33",
|
||||||
"label": "Frame",
|
"label": "Frame",
|
||||||
"tabIndex": 33,
|
"tabIndex": 33,
|
||||||
"category": "Settings",
|
"category": "Frame",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
"frame",
|
"frame",
|
||||||
"settings"
|
"window"
|
||||||
],
|
],
|
||||||
"icon": "frame_source"
|
"icon": "frame_source"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "frameEnabled",
|
||||||
|
"label": "Frame",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"around",
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"decoration",
|
||||||
|
"display",
|
||||||
|
"draw",
|
||||||
|
"entire",
|
||||||
|
"frame",
|
||||||
|
"monitor",
|
||||||
|
"outline",
|
||||||
|
"output",
|
||||||
|
"picture",
|
||||||
|
"screen",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "frame_source",
|
||||||
|
"description": "Draw a connected picture-frame border around the entire display"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameBlurEnabled",
|
||||||
|
"label": "Frame Blur",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"background",
|
||||||
|
"blur",
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"frosted",
|
||||||
|
"glass",
|
||||||
|
"newer",
|
||||||
|
"quickshell",
|
||||||
|
"requires",
|
||||||
|
"transparency",
|
||||||
|
"version",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Requires a newer version of Quickshell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameBarIntegration",
|
||||||
|
"label": "Integrations",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"during",
|
||||||
|
"frame",
|
||||||
|
"hide",
|
||||||
|
"integrations",
|
||||||
|
"niri",
|
||||||
|
"overview",
|
||||||
|
"show",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "toolbar",
|
||||||
|
"description": "Show during Niri overview",
|
||||||
|
"conditionKey": "isNiri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameLauncherEmergeSide",
|
||||||
|
"label": "Launcher Emerge Side",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"app drawer",
|
||||||
|
"app menu",
|
||||||
|
"applications",
|
||||||
|
"border",
|
||||||
|
"bottom",
|
||||||
|
"connected",
|
||||||
|
"decoration",
|
||||||
|
"direction",
|
||||||
|
"edge",
|
||||||
|
"emerge",
|
||||||
|
"frame",
|
||||||
|
"launcher",
|
||||||
|
"modal",
|
||||||
|
"side",
|
||||||
|
"slides",
|
||||||
|
"start menu",
|
||||||
|
"top",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Edge the launcher slides from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameMode",
|
||||||
|
"label": "Mode",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"decoration",
|
||||||
|
"emerge",
|
||||||
|
"flush",
|
||||||
|
"frame",
|
||||||
|
"mode",
|
||||||
|
"panel",
|
||||||
|
"popout",
|
||||||
|
"separate",
|
||||||
|
"statusbar",
|
||||||
|
"surfaces",
|
||||||
|
"taskbar",
|
||||||
|
"topbar",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"icon": "tune",
|
||||||
|
"description": "Surfaces emerge flush from the bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameShowOnOverview",
|
||||||
|
"label": "Show on Overview",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"during",
|
||||||
|
"frame",
|
||||||
|
"hide",
|
||||||
|
"niri",
|
||||||
|
"overview",
|
||||||
|
"show",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Show during Niri overview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameBarSize",
|
||||||
|
"label": "Size",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"bar",
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"height",
|
||||||
|
"horizontal",
|
||||||
|
"panel",
|
||||||
|
"size",
|
||||||
|
"statusbar",
|
||||||
|
"taskbar",
|
||||||
|
"thickness",
|
||||||
|
"topbar",
|
||||||
|
"vertical",
|
||||||
|
"width",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Horizontal and vertical bar thickness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameModeSelector",
|
||||||
|
"label": "Surface Behavior",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"behavior",
|
||||||
|
"border",
|
||||||
|
"connected",
|
||||||
|
"decoration",
|
||||||
|
"emerge",
|
||||||
|
"flush",
|
||||||
|
"frame",
|
||||||
|
"mode",
|
||||||
|
"panel",
|
||||||
|
"popout",
|
||||||
|
"separate",
|
||||||
|
"statusbar",
|
||||||
|
"surface",
|
||||||
|
"surfaces",
|
||||||
|
"taskbar",
|
||||||
|
"topbar",
|
||||||
|
"window"
|
||||||
|
],
|
||||||
|
"description": "Surfaces emerge flush from the bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "frameOpacity",
|
||||||
|
"label": "Surface Opacity",
|
||||||
|
"tabIndex": 33,
|
||||||
|
"category": "Frame",
|
||||||
|
"keywords": [
|
||||||
|
"border",
|
||||||
|
"decoration",
|
||||||
|
"frame",
|
||||||
|
"opacity",
|
||||||
|
"popup",
|
||||||
|
"surface",
|
||||||
|
"transparency",
|
||||||
|
"window"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "_tab_34",
|
"section": "_tab_34",
|
||||||
"label": "Applications",
|
"label": "Applications",
|
||||||
"tabIndex": 34,
|
"tabIndex": 34,
|
||||||
"category": "Settings",
|
"category": "Default Apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"app",
|
"app",
|
||||||
"applications",
|
"applications",
|
||||||
"apps",
|
"apps",
|
||||||
|
"browser",
|
||||||
|
"default",
|
||||||
|
"handlers",
|
||||||
|
"mime",
|
||||||
"program",
|
"program",
|
||||||
"programs",
|
"programs",
|
||||||
"settings"
|
"terminal"
|
||||||
],
|
],
|
||||||
"icon": "apps"
|
"icon": "apps"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "createUserGreeter",
|
||||||
|
"label": "Allow greeter login access",
|
||||||
|
"tabIndex": 35,
|
||||||
|
"category": "Users",
|
||||||
|
"keywords": [
|
||||||
|
"access",
|
||||||
|
"accounts",
|
||||||
|
"allow",
|
||||||
|
"greeter",
|
||||||
|
"group",
|
||||||
|
"login",
|
||||||
|
"profile",
|
||||||
|
"sync",
|
||||||
|
"they",
|
||||||
|
"user",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"description": "Add the new user to the %1 group so they can run dms greeter sync --profile."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "createUser",
|
||||||
|
"label": "Create User",
|
||||||
|
"tabIndex": 35,
|
||||||
|
"category": "Users",
|
||||||
|
"keywords": [
|
||||||
|
"accounts",
|
||||||
|
"admin",
|
||||||
|
"create",
|
||||||
|
"group",
|
||||||
|
"profile",
|
||||||
|
"sudo",
|
||||||
|
"they",
|
||||||
|
"user",
|
||||||
|
"users",
|
||||||
|
"wheel"
|
||||||
|
],
|
||||||
|
"icon": "person_add",
|
||||||
|
"description": "Add the new user to the %1 group so they can use sudo."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "usersList",
|
||||||
|
"label": "Existing Users",
|
||||||
|
"tabIndex": 35,
|
||||||
|
"category": "Users",
|
||||||
|
"keywords": [
|
||||||
|
"accounts",
|
||||||
|
"existing",
|
||||||
|
"profile",
|
||||||
|
"user",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"icon": "group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "createUserAdmin",
|
||||||
|
"label": "Grant administrator privileges",
|
||||||
|
"tabIndex": 35,
|
||||||
|
"category": "Users",
|
||||||
|
"keywords": [
|
||||||
|
"accounts",
|
||||||
|
"admin",
|
||||||
|
"administrator",
|
||||||
|
"grant",
|
||||||
|
"group",
|
||||||
|
"privileges",
|
||||||
|
"profile",
|
||||||
|
"sudo",
|
||||||
|
"they",
|
||||||
|
"user",
|
||||||
|
"users",
|
||||||
|
"wheel"
|
||||||
|
],
|
||||||
|
"description": "Add the new user to the %1 group so they can use sudo."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "_tab_35",
|
"section": "_tab_35",
|
||||||
"label": "Users",
|
"label": "Users",
|
||||||
"tabIndex": 35,
|
"tabIndex": 35,
|
||||||
"category": "Settings",
|
"category": "Users",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"settings",
|
"accounts",
|
||||||
|
"profile",
|
||||||
|
"user",
|
||||||
"users"
|
"users"
|
||||||
],
|
],
|
||||||
"icon": "manage_accounts"
|
"icon": "manage_accounts"
|
||||||
@@ -7754,11 +8615,27 @@
|
|||||||
"section": "_tab_36",
|
"section": "_tab_36",
|
||||||
"label": "Autostart Apps",
|
"label": "Autostart Apps",
|
||||||
"tabIndex": 36,
|
"tabIndex": 36,
|
||||||
"category": "Settings",
|
"category": "Autostart",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"apps",
|
"apps",
|
||||||
"autostart",
|
"autostart",
|
||||||
"settings"
|
"boot",
|
||||||
|
"launch",
|
||||||
|
"startup"
|
||||||
|
],
|
||||||
|
"icon": "line_start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "autostartEntries",
|
||||||
|
"label": "Autostart Entries",
|
||||||
|
"tabIndex": 36,
|
||||||
|
"category": "Autostart",
|
||||||
|
"keywords": [
|
||||||
|
"autostart",
|
||||||
|
"boot",
|
||||||
|
"entries",
|
||||||
|
"launch",
|
||||||
|
"startup"
|
||||||
],
|
],
|
||||||
"icon": "line_start"
|
"icon": "line_start"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user