1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 16:02:51 -05:00

keybinds: initial support for writable hyprland and mangoWC

fixes #1204
This commit is contained in:
bbedward
2026-01-07 12:15:38 -05:00
parent e822fa73da
commit a205df1bd6
16 changed files with 2372 additions and 287 deletions

View File

@@ -106,8 +106,8 @@ windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture
windowrule = float on, match:class ^(zoom)$ windowrule = float on, match:class ^(zoom)$
# DMS windows floating by default # DMS windows floating by default
windowrule = float on, match:class ^(org.quickshell)$ # ! Hyprland doesnt size these windows correctly so disabling by default here
windowrule = opacity 0.9 0.9, match:float false, match:focus false # windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^(quickshell)$

View File

@@ -2,45 +2,93 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type HyprlandProvider struct { type HyprlandProvider struct {
configPath string configPath string
dmsBindsIncluded bool
parsed bool
} }
func NewHyprlandProvider(configPath string) *HyprlandProvider { func NewHyprlandProvider(configPath string) *HyprlandProvider {
if configPath == "" { if configPath == "" {
configPath = "$HOME/.config/hypr" configPath = defaultHyprlandConfigDir()
} }
return &HyprlandProvider{ return &HyprlandProvider{
configPath: configPath, configPath: configPath,
} }
} }
func defaultHyprlandConfigDir() string {
configDir, err := os.UserConfigDir()
if err != nil {
return ""
}
return filepath.Join(configDir, "hypr")
}
func (h *HyprlandProvider) Name() string { func (h *HyprlandProvider) Name() string {
return "hyprland" return "hyprland"
} }
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := ParseHyprlandKeys(h.configPath) result, err := ParseHyprlandKeysWithDMS(h.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err) return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
} }
categorizedBinds := make(map[string][]keybinds.Keybind) h.dmsBindsIncluded = result.DMSBindsIncluded
h.convertSection(section, "", categorizedBinds) h.parsed = true
return &keybinds.CheatSheet{ categorizedBinds := make(map[string][]keybinds.Keybind)
Title: "Hyprland Keybinds", h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
Provider: h.Name(),
Binds: categorizedBinds, sheet := &keybinds.CheatSheet{
}, nil Title: "Hyprland Keybinds",
Provider: h.Name(),
Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
} }
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (h *HyprlandProvider) HasDMSBindsIncluded() bool {
if h.parsed {
return h.dmsBindsIncluded
}
result, err := ParseHyprlandKeysWithDMS(h.configPath)
if err != nil {
return false
}
h.dmsBindsIncluded = result.DMSBindsIncluded
h.parsed = true
return h.dmsBindsIncluded
}
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -48,12 +96,12 @@ func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory
for _, kb := range section.Keybinds { for _, kb := range section.Keybinds {
category := h.categorizeByDispatcher(kb.Dispatcher) category := h.categorizeByDispatcher(kb.Dispatcher)
bind := h.convertKeybind(&kb, currentSubcat) bind := h.convertKeybind(&kb, currentSubcat, conflicts)
categorizedBinds[category] = append(categorizedBinds[category], bind) categorizedBinds[category] = append(categorizedBinds[category], bind)
} }
for _, child := range section.Children { for _, child := range section.Children {
h.convertSection(&child, currentSubcat, categorizedBinds) h.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
} }
} }
@@ -85,8 +133,8 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
} }
} }
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind { func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind {
key := h.formatKey(kb) keyStr := h.formatKey(kb)
rawAction := h.formatRawAction(kb.Dispatcher, kb.Params) rawAction := h.formatRawAction(kb.Dispatcher, kb.Params)
desc := kb.Comment desc := kb.Comment
@@ -94,12 +142,32 @@ func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory st
desc = rawAction desc = rawAction
} }
return keybinds.Keybind{ source := "config"
Key: key, if strings.Contains(kb.Source, "dms/binds.conf") {
source = "dms"
}
bind := keybinds.Keybind{
Key: keyStr,
Description: desc, Description: desc,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source,
} }
if source == "dms" && conflicts != nil {
normalizedKey := strings.ToLower(keyStr)
if conflictKb, ok := conflicts[normalizedKey]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Comment,
Action: h.formatRawAction(conflictKb.Dispatcher, conflictKb.Params),
Source: "config",
}
}
}
return bind
} }
func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string { func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string {
@@ -115,3 +183,262 @@ func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
parts = append(parts, kb.Key) parts = append(parts, kb.Key)
return strings.Join(parts, "+") return strings.Join(parts, "+")
} }
func (h *HyprlandProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(h.configPath)
if err != nil {
return filepath.Join(h.configPath, "dms", "binds.conf")
}
return filepath.Join(expanded, "dms", "binds.conf")
}
func (h *HyprlandProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "exec" || action == "exec ":
return fmt.Errorf("exec dispatcher requires arguments")
case strings.HasPrefix(action, "exec "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "exec "))
if rest == "" {
return fmt.Errorf("exec dispatcher requires arguments")
}
}
return nil
}
func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error {
if err := h.validateAction(action); err != nil {
return err
}
overridePath := h.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := h.loadOverrideBinds()
if err != nil {
existingBinds = make(map[string]*hyprlandOverrideBind)
}
normalizedKey := strings.ToLower(key)
existingBinds[normalizedKey] = &hyprlandOverrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
}
return h.writeOverrideBinds(existingBinds)
}
func (h *HyprlandProvider) RemoveBind(key string) error {
existingBinds, err := h.loadOverrideBinds()
if err != nil {
return nil
}
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return h.writeOverrideBinds(existingBinds)
}
type hyprlandOverrideBind struct {
Key string
Action string
Description string
Options map[string]any
}
func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) {
overridePath := h.GetOverridePath()
binds := make(map[string]*hyprlandOverrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !strings.HasPrefix(line, "bind") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
var comment string
if len(commentParts) > 1 {
comment = strings.TrimSpace(commentParts[1])
}
fields := strings.SplitN(bindContent, ",", 4)
if len(fields) < 3 {
continue
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
dispatcher := strings.TrimSpace(fields[2])
var params string
if len(fields) > 3 {
params = strings.TrimSpace(fields[3])
}
keyStr := h.buildKeyString(mods, keyName)
normalizedKey := strings.ToLower(keyStr)
action := dispatcher
if params != "" {
action = dispatcher + " " + params
}
binds[normalizedKey] = &hyprlandOverrideBind{
Key: keyStr,
Action: action,
Description: comment,
}
}
return binds, nil
}
func (h *HyprlandProvider) buildKeyString(mods, key string) string {
if mods == "" {
return key
}
modList := strings.FieldsFunc(mods, func(r rune) bool {
return r == '+' || r == ' '
})
parts := append(modList, key)
return strings.Join(parts, "+")
}
func (h *HyprlandProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "focus") ||
strings.Contains(action, "move") || strings.Contains(action, "swap") ||
strings.Contains(action, "resize"):
return 2
case strings.Contains(action, "monitor"):
return 3
case strings.HasPrefix(action, "exec"):
return 4
case action == "exit" || strings.Contains(action, "dpms"):
return 5
default:
return 6
}
}
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
overridePath := h.GetOverridePath()
content := h.generateBindsContent(binds)
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
if len(binds) == 0 {
return ""
}
bindList := make([]*hyprlandOverrideBind, 0, len(binds))
for _, bind := range binds {
bindList = append(bindList, bind)
}
sort.Slice(bindList, func(i, j int) bool {
pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action)
if pi != pj {
return pi < pj
}
return bindList[i].Key < bindList[j].Key
})
var sb strings.Builder
for _, bind := range bindList {
h.writeBindLine(&sb, bind)
}
return sb.String()
}
func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) {
mods, key := h.parseKeyString(bind.Key)
dispatcher, params := h.parseAction(bind.Action)
sb.WriteString("bind = ")
sb.WriteString(mods)
sb.WriteString(", ")
sb.WriteString(key)
sb.WriteString(", ")
sb.WriteString(dispatcher)
if params != "" {
sb.WriteString(", ")
sb.WriteString(params)
}
if bind.Description != "" {
sb.WriteString(" # ")
sb.WriteString(bind.Description)
}
sb.WriteString("\n")
}
func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+")
switch len(parts) {
case 0:
return "", keyStr
case 1:
return "", parts[0]
default:
return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1]
}
}
func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) {
parts := strings.SplitN(action, " ", 2)
switch len(parts) {
case 0:
return action, ""
case 1:
dispatcher = parts[0]
default:
dispatcher = parts[0]
params = parts[1]
}
// Convert internal spawn format to Hyprland's exec
if dispatcher == "spawn" {
dispatcher = "exec"
}
return dispatcher, params
}

View File

@@ -23,6 +23,7 @@ type HyprlandKeyBinding struct {
Dispatcher string `json:"dispatcher"` Dispatcher string `json:"dispatcher"`
Params string `json:"params"` Params string `json:"params"`
Comment string `json:"comment"` Comment string `json:"comment"`
Source string `json:"source"`
} }
type HyprlandSection struct { type HyprlandSection struct {
@@ -32,14 +33,36 @@ type HyprlandSection struct {
} }
type HyprlandParser struct { type HyprlandParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
configDir string
currentSource string
dmsBindsExists bool
dmsBindsIncluded bool
includeCount int
dmsIncludePos int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
conflictingConfigs map[string]*HyprlandKeyBinding
bindMap map[string]*HyprlandKeyBinding
bindOrder []string
processedFiles map[string]bool
dmsProcessed bool
} }
func NewHyprlandParser() *HyprlandParser { func NewHyprlandParser(configDir string) *HyprlandParser {
return &HyprlandParser{ return &HyprlandParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
configDir: configDir,
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
conflictingConfigs: make(map[string]*HyprlandKeyBinding),
bindMap: make(map[string]*HyprlandKeyBinding),
bindOrder: []string{},
processedFiles: make(map[string]bool),
} }
} }
@@ -320,9 +343,308 @@ func (p *HyprlandParser) ParseKeys() *HyprlandSection {
} }
func ParseHyprlandKeys(path string) (*HyprlandSection, error) { func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
parser := NewHyprlandParser() parser := NewHyprlandParser(path)
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }
return parser.ParseKeys(), nil return parser.ParseKeys(), nil
} }
type HyprlandParseResult struct {
Section *HyprlandSection
DMSBindsIncluded bool
DMSStatus *HyprlandDMSStatus
ConflictingConfigs map[string]*HyprlandKeyBinding
}
type HyprlandDMSStatus struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus {
status := &HyprlandDMSStatus{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.conf does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.conf is not sourced in config"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
}
func (p *HyprlandParser) formatBindKey(kb *HyprlandKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *HyprlandParser) normalizeKey(key string) string {
return strings.ToLower(key)
}
func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool {
key := p.formatBindKey(kb)
normalizedKey := p.normalizeKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf")
if isDMSBind {
p.dmsBindKeys[normalizedKey] = true
} else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++
p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[normalizedKey] = true
return false
} else {
p.configBindKeys[normalizedKey] = true
}
if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[normalizedKey] = kb
return true
}
func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) {
expandedDir, err := utils.ExpandPath(p.configDir)
if err != nil {
return nil, err
}
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
mainConfig := filepath.Join(expandedDir, "hyprland.conf")
section, err := p.parseFileWithSource(mainConfig, "")
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
return section, nil
}
func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
if p.processedFiles[absPath] {
return &HyprlandSection{Name: sectionName}, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
prevSource := p.currentSource
p.currentSource = absPath
section := &HyprlandSection{Name: sectionName}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "source") {
p.handleSource(trimmed, section, filepath.Dir(absPath))
continue
}
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.parseBindLine(line)
if kb == nil {
continue
}
kb.Source = p.currentSource
if p.addBind(kb) {
section.Keybinds = append(section.Keybinds, *kb)
}
}
p.currentSource = prevSource
return section, nil
}
func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, baseDir string) {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
return
}
sourcePath := strings.TrimSpace(parts[1])
isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
p.includeCount++
if isDMSSource {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
return
}
includedSection, err := p.parseFileWithSource(expanded, "")
if err != nil {
return
}
section.Children = append(section.Children, *includedSection)
}
func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.parseBindLine(line)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
if p.addBind(kb) {
section.Keybinds = append(section.Keybinds, *kb)
}
}
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
return nil
}
keys := parts[1]
keyParts := strings.SplitN(keys, "#", 2)
keys = keyParts[0]
var comment string
if len(keyParts) > 1 {
comment = strings.TrimSpace(keyParts[1])
}
keyFields := strings.SplitN(keys, ",", 5)
if len(keyFields) < 3 {
return nil
}
mods := strings.TrimSpace(keyFields[0])
key := strings.TrimSpace(keyFields[1])
dispatcher := strings.TrimSpace(keyFields[2])
var params string
if len(keyFields) > 3 {
paramParts := keyFields[3:]
params = strings.TrimSpace(strings.Join(paramParts, ","))
}
if comment != "" && strings.HasPrefix(comment, HideComment) {
return nil
}
if comment == "" {
comment = hyprlandAutogenerateComment(dispatcher, params)
}
var modList []string
if mods != "" {
modstring := mods + string(ModSeparators[0])
idx := 0
for index, char := range modstring {
isModSep := false
for _, sep := range ModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-idx > 1 {
modList = append(modList, modstring[idx:index])
}
idx = index + 1
}
}
}
return &HyprlandKeyBinding{
Mods: modList,
Key: key,
Dispatcher: dispatcher,
Params: params,
Comment: comment,
}
}
func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) {
parser := NewHyprlandParser(path)
section, err := parser.ParseWithDMS()
if err != nil {
return nil, err
}
return &HyprlandParseResult{
Section: section,
DMSBindsIncluded: parser.dmsBindsIncluded,
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -130,7 +130,7 @@ func TestHyprlandGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewHyprlandParser() parser := NewHyprlandParser("")
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)
@@ -285,7 +285,7 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewHyprlandParser() parser := NewHyprlandParser("")
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -343,7 +343,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewHyprlandParser() parser := NewHyprlandParser("")
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -353,7 +353,7 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
} }
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) { func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
parser := NewHyprlandParser() parser := NewHyprlandParser("")
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"} parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
result := parser.getKeybindAtLine(0) result := parser.getKeybindAtLine(0)

View File

@@ -7,35 +7,30 @@ import (
) )
func TestNewHyprlandProvider(t *testing.T) { func TestNewHyprlandProvider(t *testing.T) {
tests := []struct { t.Run("custom path", func(t *testing.T) {
name string p := NewHyprlandProvider("/custom/path")
configPath string if p == nil {
wantPath string t.Fatal("NewHyprlandProvider returned nil")
}{ }
{ if p.configPath != "/custom/path" {
name: "custom path", t.Errorf("configPath = %q, want %q", p.configPath, "/custom/path")
configPath: "/custom/path", }
wantPath: "/custom/path", })
},
{
name: "empty path defaults",
configPath: "",
wantPath: "$HOME/.config/hypr",
},
}
for _, tt := range tests { t.Run("empty path defaults", func(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { p := NewHyprlandProvider("")
p := NewHyprlandProvider(tt.configPath) if p == nil {
if p == nil { t.Fatal("NewHyprlandProvider returned nil")
t.Fatal("NewHyprlandProvider returned nil") }
} configDir, err := os.UserConfigDir()
if err != nil {
if p.configPath != tt.wantPath { t.Fatalf("UserConfigDir failed: %v", err)
t.Errorf("configPath = %q, want %q", p.configPath, tt.wantPath) }
} expected := filepath.Join(configDir, "hypr")
}) if p.configPath != expected {
} t.Errorf("configPath = %q, want %q", p.configPath, expected)
}
})
} }
func TestHyprlandProviderName(t *testing.T) { func TestHyprlandProviderName(t *testing.T) {
@@ -109,7 +104,7 @@ func TestHyprlandProviderGetCheatSheetError(t *testing.T) {
func TestFormatKey(t *testing.T) { func TestFormatKey(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
tests := []struct { tests := []struct {
name string name string
@@ -163,7 +158,7 @@ func TestFormatKey(t *testing.T) {
func TestDescriptionFallback(t *testing.T) { func TestDescriptionFallback(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "test.conf") configFile := filepath.Join(tmpDir, "hyprland.conf")
tests := []struct { tests := []struct {
name string name string

View File

@@ -2,46 +2,94 @@ package providers
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
) )
type MangoWCProvider struct { type MangoWCProvider struct {
configPath string configPath string
dmsBindsIncluded bool
parsed bool
} }
func NewMangoWCProvider(configPath string) *MangoWCProvider { func NewMangoWCProvider(configPath string) *MangoWCProvider {
if configPath == "" { if configPath == "" {
configPath = "$HOME/.config/mango" configPath = defaultMangoWCConfigDir()
} }
return &MangoWCProvider{ return &MangoWCProvider{
configPath: configPath, configPath: configPath,
} }
} }
func defaultMangoWCConfigDir() string {
configDir, err := os.UserConfigDir()
if err != nil {
return ""
}
return filepath.Join(configDir, "mango")
}
func (m *MangoWCProvider) Name() string { func (m *MangoWCProvider) Name() string {
return "mangowc" return "mangowc"
} }
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := ParseMangoWCKeys(m.configPath) result, err := ParseMangoWCKeysWithDMS(m.configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err) return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
} }
m.dmsBindsIncluded = result.DMSBindsIncluded
m.parsed = true
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range keybinds_list { for _, kb := range result.Keybinds {
category := m.categorizeByCommand(kb.Command) category := m.categorizeByCommand(kb.Command)
bind := m.convertKeybind(&kb) bind := m.convertKeybind(&kb, result.ConflictingConfigs)
categorizedBinds[category] = append(categorizedBinds[category], bind) categorizedBinds[category] = append(categorizedBinds[category], bind)
} }
return &keybinds.CheatSheet{ sheet := &keybinds.CheatSheet{
Title: "MangoWC Keybinds", Title: "MangoWC Keybinds",
Provider: m.Name(), Provider: m.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
}, nil DMSBindsIncluded: result.DMSBindsIncluded,
}
if result.DMSStatus != nil {
sheet.DMSStatus = &keybinds.DMSBindsStatus{
Exists: result.DMSStatus.Exists,
Included: result.DMSStatus.Included,
IncludePosition: result.DMSStatus.IncludePosition,
TotalIncludes: result.DMSStatus.TotalIncludes,
BindsAfterDMS: result.DMSStatus.BindsAfterDMS,
Effective: result.DMSStatus.Effective,
OverriddenBy: result.DMSStatus.OverriddenBy,
StatusMessage: result.DMSStatus.StatusMessage,
}
}
return sheet, nil
}
func (m *MangoWCProvider) HasDMSBindsIncluded() bool {
if m.parsed {
return m.dmsBindsIncluded
}
result, err := ParseMangoWCKeysWithDMS(m.configPath)
if err != nil {
return false
}
m.dmsBindsIncluded = result.DMSBindsIncluded
m.parsed = true
return m.dmsBindsIncluded
} }
func (m *MangoWCProvider) categorizeByCommand(command string) string { func (m *MangoWCProvider) categorizeByCommand(command string) string {
@@ -82,8 +130,8 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
} }
} }
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind { func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[string]*MangoWCKeyBinding) keybinds.Keybind {
key := m.formatKey(kb) keyStr := m.formatKey(kb)
rawAction := m.formatRawAction(kb.Command, kb.Params) rawAction := m.formatRawAction(kb.Command, kb.Params)
desc := kb.Comment desc := kb.Comment
@@ -91,11 +139,31 @@ func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind
desc = rawAction desc = rawAction
} }
return keybinds.Keybind{ source := "config"
Key: key, if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") {
source = "dms"
}
bind := keybinds.Keybind{
Key: keyStr,
Description: desc, Description: desc,
Action: rawAction, Action: rawAction,
Source: source,
} }
if source == "dms" && conflicts != nil {
normalizedKey := strings.ToLower(keyStr)
if conflictKb, ok := conflicts[normalizedKey]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Comment,
Action: m.formatRawAction(conflictKb.Command, conflictKb.Params),
Source: "config",
}
}
}
return bind
} }
func (m *MangoWCProvider) formatRawAction(command, params string) string { func (m *MangoWCProvider) formatRawAction(command, params string) string {
@@ -111,3 +179,264 @@ func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
parts = append(parts, kb.Key) parts = append(parts, kb.Key)
return strings.Join(parts, "+") return strings.Join(parts, "+")
} }
func (m *MangoWCProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "dms", "binds.conf")
}
return filepath.Join(expanded, "dms", "binds.conf")
}
func (m *MangoWCProvider) validateAction(action string) error {
action = strings.TrimSpace(action)
switch {
case action == "":
return fmt.Errorf("action cannot be empty")
case action == "spawn" || action == "spawn ":
return fmt.Errorf("spawn command requires arguments")
case action == "spawn_shell" || action == "spawn_shell ":
return fmt.Errorf("spawn_shell command requires arguments")
case strings.HasPrefix(action, "spawn "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn "))
if rest == "" {
return fmt.Errorf("spawn command requires arguments")
}
case strings.HasPrefix(action, "spawn_shell "):
rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn_shell "))
if rest == "" {
return fmt.Errorf("spawn_shell command requires arguments")
}
}
return nil
}
func (m *MangoWCProvider) SetBind(key, action, description string, options map[string]any) error {
if err := m.validateAction(action); err != nil {
return err
}
overridePath := m.GetOverridePath()
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
return fmt.Errorf("failed to create dms directory: %w", err)
}
existingBinds, err := m.loadOverrideBinds()
if err != nil {
existingBinds = make(map[string]*mangowcOverrideBind)
}
normalizedKey := strings.ToLower(key)
existingBinds[normalizedKey] = &mangowcOverrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
}
return m.writeOverrideBinds(existingBinds)
}
func (m *MangoWCProvider) RemoveBind(key string) error {
existingBinds, err := m.loadOverrideBinds()
if err != nil {
return nil
}
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return m.writeOverrideBinds(existingBinds)
}
type mangowcOverrideBind struct {
Key string
Action string
Description string
Options map[string]any
}
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
overridePath := m.GetOverridePath()
binds := make(map[string]*mangowcOverrideBind)
data, err := os.ReadFile(overridePath)
if os.IsNotExist(err) {
return binds, nil
}
if err != nil {
return nil, err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !strings.HasPrefix(line, "bind") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
var comment string
if len(commentParts) > 1 {
comment = strings.TrimSpace(commentParts[1])
}
fields := strings.SplitN(bindContent, ",", 4)
if len(fields) < 3 {
continue
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
command := strings.TrimSpace(fields[2])
var params string
if len(fields) > 3 {
params = strings.TrimSpace(fields[3])
}
keyStr := m.buildKeyString(mods, keyName)
normalizedKey := strings.ToLower(keyStr)
action := command
if params != "" {
action = command + " " + params
}
binds[normalizedKey] = &mangowcOverrideBind{
Key: keyStr,
Action: action,
Description: comment,
}
}
return binds, nil
}
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
if mods == "" || strings.EqualFold(mods, "none") {
return key
}
modList := strings.FieldsFunc(mods, func(r rune) bool {
return r == '+' || r == ' '
})
parts := append(modList, key)
return strings.Join(parts, "+")
}
func (m *MangoWCProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "view") || strings.Contains(action, "tag"):
return 1
case strings.Contains(action, "focus") || strings.Contains(action, "exchange") ||
strings.Contains(action, "resize") || strings.Contains(action, "move"):
return 2
case strings.Contains(action, "mon"):
return 3
case strings.HasPrefix(action, "spawn"):
return 4
case action == "quit" || action == "reload_config":
return 5
default:
return 6
}
}
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
overridePath := m.GetOverridePath()
content := m.generateBindsContent(binds)
return os.WriteFile(overridePath, []byte(content), 0644)
}
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
if len(binds) == 0 {
return ""
}
bindList := make([]*mangowcOverrideBind, 0, len(binds))
for _, bind := range binds {
bindList = append(bindList, bind)
}
sort.Slice(bindList, func(i, j int) bool {
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
if pi != pj {
return pi < pj
}
return bindList[i].Key < bindList[j].Key
})
var sb strings.Builder
for _, bind := range bindList {
m.writeBindLine(&sb, bind)
}
return sb.String()
}
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
mods, key := m.parseKeyString(bind.Key)
command, params := m.parseAction(bind.Action)
sb.WriteString("bind=")
if mods == "" {
sb.WriteString("none")
} else {
sb.WriteString(mods)
}
sb.WriteString(",")
sb.WriteString(key)
sb.WriteString(",")
sb.WriteString(command)
if params != "" {
sb.WriteString(",")
sb.WriteString(params)
}
if bind.Description != "" {
sb.WriteString(" # ")
sb.WriteString(bind.Description)
}
sb.WriteString("\n")
}
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+")
switch len(parts) {
case 0:
return "", keyStr
case 1:
return "", parts[0]
default:
return strings.Join(parts[:len(parts)-1], "+"), parts[len(parts)-1]
}
}
func (m *MangoWCProvider) parseAction(action string) (command, params string) {
parts := strings.SplitN(action, " ", 2)
switch len(parts) {
case 0:
return action, ""
case 1:
return parts[0], ""
default:
return parts[0], parts[1]
}
}

View File

@@ -21,17 +21,40 @@ type MangoWCKeyBinding struct {
Command string `json:"command"` Command string `json:"command"`
Params string `json:"params"` Params string `json:"params"`
Comment string `json:"comment"` Comment string `json:"comment"`
Source string `json:"source"`
} }
type MangoWCParser struct { type MangoWCParser struct {
contentLines []string contentLines []string
readingLine int readingLine int
configDir string
currentSource string
dmsBindsExists bool
dmsBindsIncluded bool
includeCount int
dmsIncludePos int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
conflictingConfigs map[string]*MangoWCKeyBinding
bindMap map[string]*MangoWCKeyBinding
bindOrder []string
processedFiles map[string]bool
dmsProcessed bool
} }
func NewMangoWCParser() *MangoWCParser { func NewMangoWCParser(configDir string) *MangoWCParser {
return &MangoWCParser{ return &MangoWCParser{
contentLines: []string{}, contentLines: []string{},
readingLine: 0, readingLine: 0,
configDir: configDir,
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
conflictingConfigs: make(map[string]*MangoWCKeyBinding),
bindMap: make(map[string]*MangoWCKeyBinding),
bindOrder: []string{},
processedFiles: make(map[string]bool),
} }
} }
@@ -294,9 +317,320 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
} }
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) { func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
parser := NewMangoWCParser() parser := NewMangoWCParser(path)
if err := parser.ReadContent(path); err != nil { if err := parser.ReadContent(path); err != nil {
return nil, err return nil, err
} }
return parser.ParseKeys(), nil return parser.ParseKeys(), nil
} }
type MangoWCParseResult struct {
Keybinds []MangoWCKeyBinding
DMSBindsIncluded bool
DMSStatus *MangoWCDMSStatus
ConflictingConfigs map[string]*MangoWCKeyBinding
}
type MangoWCDMSStatus struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *MangoWCParser) buildDMSStatus() *MangoWCDMSStatus {
status := &MangoWCDMSStatus{
Exists: p.dmsBindsExists,
Included: p.dmsBindsIncluded,
IncludePosition: p.dmsIncludePos,
TotalIncludes: p.includeCount,
BindsAfterDMS: p.bindsAfterDMS,
}
switch {
case !p.dmsBindsExists:
status.Effective = false
status.StatusMessage = "dms/binds.conf does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.conf is not sourced in config"
case p.bindsAfterDMS > 0:
status.Effective = true
status.OverriddenBy = p.bindsAfterDMS
status.StatusMessage = "Some DMS binds may be overridden by config binds"
default:
status.Effective = true
status.StatusMessage = "DMS binds are active"
}
return status
}
func (p *MangoWCParser) formatBindKey(kb *MangoWCKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (p *MangoWCParser) normalizeKey(key string) string {
return strings.ToLower(key)
}
func (p *MangoWCParser) addBind(kb *MangoWCKeyBinding) {
key := p.formatBindKey(kb)
normalizedKey := p.normalizeKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(os.PathSeparator)+"binds.conf")
if isDMSBind {
p.dmsBindKeys[normalizedKey] = true
} else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++
p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[normalizedKey] = true
return
} else {
p.configBindKeys[normalizedKey] = true
}
if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, key)
}
p.bindMap[normalizedKey] = kb
}
func (p *MangoWCParser) ParseWithDMS() ([]MangoWCKeyBinding, error) {
expandedDir, err := utils.ExpandPath(p.configDir)
if err != nil {
return nil, err
}
dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
mainConfig := filepath.Join(expandedDir, "config.conf")
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
mainConfig = filepath.Join(expandedDir, "mango.conf")
}
_, err = p.parseFileWithSource(mainConfig)
if err != nil {
return nil, err
}
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath)
}
var keybinds []MangoWCKeyBinding
for _, key := range p.bindOrder {
normalizedKey := p.normalizeKey(key)
if kb, exists := p.bindMap[normalizedKey]; exists {
keybinds = append(keybinds, *kb)
}
}
return keybinds, nil
}
func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBinding, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return nil, err
}
if p.processedFiles[absPath] {
return nil, nil
}
p.processedFiles[absPath] = true
data, err := os.ReadFile(absPath)
if err != nil {
return nil, err
}
prevSource := p.currentSource
p.currentSource = absPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "source") {
p.handleSource(trimmed, filepath.Dir(absPath), &keybinds)
continue
}
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = p.currentSource
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
return keybinds, nil
}
func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKeyBinding) {
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
return
}
sourcePath := strings.TrimSpace(parts[1])
isDMSSource := sourcePath == "dms/binds.conf" || sourcePath == "./dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf")
p.includeCount++
if isDMSSource {
p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.dmsProcessed = true
}
fullPath := sourcePath
if !filepath.IsAbs(sourcePath) {
fullPath = filepath.Join(baseDir, sourcePath)
}
expanded, err := utils.ExpandPath(fullPath)
if err != nil {
return
}
includedBinds, err := p.parseFileWithSource(expanded)
if err != nil {
return
}
*keybinds = append(*keybinds, includedBinds...)
}
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return nil
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
var keybinds []MangoWCKeyBinding
lines := strings.Split(string(data), "\n")
for lineNum, line := range lines {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "bind") {
continue
}
kb := p.getKeybindAtLineContent(line, lineNum)
if kb == nil {
continue
}
kb.Source = dmsBindsPath
p.addBind(kb)
keybinds = append(keybinds, *kb)
}
p.currentSource = prevSource
p.dmsProcessed = true
return keybinds
}
func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding {
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
}
content := matches[2]
parts := strings.SplitN(content, "#", 2)
keys := parts[0]
var comment string
if len(parts) > 1 {
comment = strings.TrimSpace(parts[1])
}
if strings.HasPrefix(comment, MangoWCHideComment) {
return nil
}
keyFields := strings.SplitN(keys, ",", 4)
if len(keyFields) < 3 {
return nil
}
mods := strings.TrimSpace(keyFields[0])
key := strings.TrimSpace(keyFields[1])
command := strings.TrimSpace(keyFields[2])
var params string
if len(keyFields) > 3 {
params = strings.TrimSpace(keyFields[3])
}
if comment == "" {
comment = mangowcAutogenerateComment(command, params)
}
var modList []string
if mods != "" && !strings.EqualFold(mods, "none") {
modstring := mods + string(MangoWCModSeparators[0])
idx := 0
for index, char := range modstring {
isModSep := false
for _, sep := range MangoWCModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-idx > 1 {
modList = append(modList, modstring[idx:index])
}
idx = index + 1
}
}
}
return &MangoWCKeyBinding{
Mods: modList,
Key: key,
Command: command,
Params: params,
Comment: comment,
}
}
func ParseMangoWCKeysWithDMS(path string) (*MangoWCParseResult, error) {
parser := NewMangoWCParser(path)
keybinds, err := parser.ParseWithDMS()
if err != nil {
return nil, err
}
return &MangoWCParseResult{
Keybinds: keybinds,
DMSBindsIncluded: parser.dmsBindsIncluded,
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil
}

View File

@@ -172,7 +172,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
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)
@@ -283,7 +283,7 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
t.Fatalf("Failed to write file2: %v", err) t.Fatalf("Failed to write file2: %v", err)
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
if err := parser.ReadContent(tmpDir); err != nil { if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -304,7 +304,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
t.Fatalf("Failed to write config: %v", err) t.Fatalf("Failed to write config: %v", err)
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
if err := parser.ReadContent(configFile); err != nil { if err := parser.ReadContent(configFile); err != nil {
t.Fatalf("ReadContent failed: %v", err) t.Fatalf("ReadContent failed: %v", err)
} }
@@ -362,7 +362,7 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
t.Skip("Cannot create relative path") t.Skip("Cannot create relative path")
} }
parser := NewMangoWCParser() parser := NewMangoWCParser("")
tildePathMatch := "~/" + relPath tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch) err = parser.ReadContent(tildePathMatch)
@@ -419,7 +419,7 @@ func TestMangoWCInvalidBindLines(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
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)

View File

@@ -15,8 +15,17 @@ func TestMangoWCProviderName(t *testing.T) {
func TestMangoWCProviderDefaultPath(t *testing.T) { func TestMangoWCProviderDefaultPath(t *testing.T) {
provider := NewMangoWCProvider("") provider := NewMangoWCProvider("")
if provider.configPath != "$HOME/.config/mango" { configDir, err := os.UserConfigDir()
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango") if err != nil {
// Fall back to testing for non-empty path
if provider.configPath == "" {
t.Error("configPath should not be empty")
}
return
}
expected := filepath.Join(configDir, "mango")
if provider.configPath != expected {
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
} }
} }
@@ -174,7 +183,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
provider := NewMangoWCProvider("") provider := NewMangoWCProvider("")
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := provider.convertKeybind(tt.keybind) result := provider.convertKeybind(tt.keybind, nil)
if result.Key != tt.wantKey { if result.Key != tt.wantKey {
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey) t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
} }

View File

@@ -103,7 +103,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" } { id: "spawn dms ipc call wallpaper prev", label: "Wallpaper: Previous" }
]; ];
const COMPOSITOR_ACTIONS = { const NIRI_ACTIONS = {
"Window": [ "Window": [
{ id: "close-window", label: "Close Window" }, { id: "close-window", label: "Close Window" },
{ id: "fullscreen-window", label: "Fullscreen" }, { id: "fullscreen-window", label: "Fullscreen" },
@@ -179,9 +179,246 @@ const COMPOSITOR_ACTIONS = {
] ]
}; };
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Window", "Monitor", "Screenshot", "System", "Overview", "Alt-Tab", "Other"]; const MANGOWC_ACTIONS = {
"Window": [
{ id: "killclient", label: "Close Window" },
{ id: "focuslast", label: "Focus Last Window" },
{ id: "focusstack next", label: "Focus Next in Stack" },
{ id: "focusstack prev", label: "Focus Previous in Stack" },
{ id: "focusdir left", label: "Focus Left" },
{ id: "focusdir right", label: "Focus Right" },
{ id: "focusdir up", label: "Focus Up" },
{ id: "focusdir down", label: "Focus Down" },
{ id: "exchange_client left", label: "Swap Left" },
{ id: "exchange_client right", label: "Swap Right" },
{ id: "exchange_client up", label: "Swap Up" },
{ id: "exchange_client down", label: "Swap Down" },
{ id: "exchange_stack_client next", label: "Swap Next in Stack" },
{ id: "exchange_stack_client prev", label: "Swap Previous in Stack" },
{ id: "togglefloating", label: "Toggle Floating" },
{ id: "togglefullscreen", label: "Toggle Fullscreen" },
{ id: "togglefakefullscreen", label: "Toggle Fake Fullscreen" },
{ id: "togglemaximizescreen", label: "Toggle Maximize" },
{ id: "toggleglobal", label: "Toggle Global (Sticky)" },
{ id: "toggleoverlay", label: "Toggle Overlay" },
{ id: "minimized", label: "Minimize Window" },
{ id: "restore_minimized", label: "Restore Minimized" },
{ id: "toggle_render_border", label: "Toggle Border" },
{ id: "centerwin", label: "Center Window" },
{ id: "zoom", label: "Swap with Master" }
],
"Move/Resize": [
{ id: "smartmovewin left", label: "Smart Move Left" },
{ id: "smartmovewin right", label: "Smart Move Right" },
{ id: "smartmovewin up", label: "Smart Move Up" },
{ id: "smartmovewin down", label: "Smart Move Down" },
{ id: "smartresizewin left", label: "Smart Resize Left" },
{ id: "smartresizewin right", label: "Smart Resize Right" },
{ id: "smartresizewin up", label: "Smart Resize Up" },
{ id: "smartresizewin down", label: "Smart Resize Down" },
{ id: "movewin", label: "Move Window (x,y)" },
{ id: "resizewin", label: "Resize Window (w,h)" }
],
"Tags": [
{ id: "view", label: "View Tag" },
{ id: "viewtoleft", label: "View Left Tag" },
{ id: "viewtoright", label: "View Right Tag" },
{ id: "viewtoleft_have_client", label: "View Left (with client)" },
{ id: "viewtoright_have_client", label: "View Right (with client)" },
{ id: "viewcrossmon", label: "View Cross-Monitor" },
{ id: "tag", label: "Move to Tag" },
{ id: "tagsilent", label: "Move to Tag (silent)" },
{ id: "tagtoleft", label: "Move to Left Tag" },
{ id: "tagtoright", label: "Move to Right Tag" },
{ id: "tagcrossmon", label: "Move Cross-Monitor" },
{ id: "toggletag", label: "Toggle Tag on Window" },
{ id: "toggleview", label: "Toggle Tag View" },
{ id: "comboview", label: "Combo View Tags" }
],
"Layout": [
{ id: "setlayout", label: "Set Layout" },
{ id: "switch_layout", label: "Cycle Layouts" },
{ id: "set_proportion", label: "Set Proportion" },
{ id: "switch_proportion_preset", label: "Cycle Proportion Presets" },
{ id: "incnmaster +1", label: "Increase Masters" },
{ id: "incnmaster -1", label: "Decrease Masters" },
{ id: "setmfact", label: "Set Master Factor" },
{ id: "incgaps", label: "Adjust Gaps" },
{ id: "togglegaps", label: "Toggle Gaps" }
],
"Monitor": [
{ id: "focusmon left", label: "Focus Monitor Left" },
{ id: "focusmon right", label: "Focus Monitor Right" },
{ id: "focusmon up", label: "Focus Monitor Up" },
{ id: "focusmon down", label: "Focus Monitor Down" },
{ id: "tagmon left", label: "Move to Monitor Left" },
{ id: "tagmon right", label: "Move to Monitor Right" },
{ id: "tagmon up", label: "Move to Monitor Up" },
{ id: "tagmon down", label: "Move to Monitor Down" },
{ id: "disable_monitor", label: "Disable Monitor" },
{ id: "enable_monitor", label: "Enable Monitor" },
{ id: "toggle_monitor", label: "Toggle Monitor" },
{ id: "create_virtual_output", label: "Create Virtual Output" },
{ id: "destroy_all_virtual_output", label: "Destroy Virtual Outputs" }
],
"Scratchpad": [
{ id: "toggle_scratchpad", label: "Toggle Scratchpad" },
{ id: "toggle_name_scratchpad", label: "Toggle Named Scratchpad" }
],
"Overview": [
{ id: "toggleoverview", label: "Toggle Overview" }
],
"System": [
{ id: "reload_config", label: "Reload Config" },
{ id: "quit", label: "Quit MangoWC" },
{ id: "setkeymode", label: "Set Keymode" },
{ id: "switch_keyboard_layout", label: "Switch Keyboard Layout" },
{ id: "setoption", label: "Set Option" },
{ id: "toggle_trackpad_enable", label: "Toggle Trackpad" }
]
};
const ACTION_ARGS = { const HYPRLAND_ACTIONS = {
"Window": [
{ id: "killactive", label: "Close Window" },
{ id: "forcekillactive", label: "Force Kill Window" },
{ id: "closewindow", label: "Close Window (by selector)" },
{ id: "killwindow", label: "Kill Window (by selector)" },
{ id: "togglefloating", label: "Toggle Floating" },
{ id: "setfloating", label: "Set Floating" },
{ id: "settiled", label: "Set Tiled" },
{ id: "fullscreen", label: "Toggle Fullscreen" },
{ id: "fullscreenstate", label: "Set Fullscreen State" },
{ id: "pin", label: "Pin Window" },
{ id: "centerwindow", label: "Center Window" },
{ id: "resizeactive", label: "Resize Active Window" },
{ id: "moveactive", label: "Move Active Window" },
{ id: "resizewindowpixel", label: "Resize Window (pixels)" },
{ id: "movewindowpixel", label: "Move Window (pixels)" },
{ id: "alterzorder", label: "Change Z-Order" },
{ id: "bringactivetotop", label: "Bring to Top" },
{ id: "setprop", label: "Set Window Property" },
{ id: "toggleswallow", label: "Toggle Swallow" }
],
"Focus": [
{ id: "movefocus l", label: "Focus Left" },
{ id: "movefocus r", label: "Focus Right" },
{ id: "movefocus u", label: "Focus Up" },
{ id: "movefocus d", label: "Focus Down" },
{ id: "movefocus", label: "Move Focus (direction)" },
{ id: "cyclenext", label: "Cycle Next Window" },
{ id: "cyclenext prev", label: "Cycle Previous Window" },
{ id: "focuswindow", label: "Focus Window (by selector)" },
{ id: "focuscurrentorlast", label: "Focus Current or Last" },
{ id: "focusurgentorlast", label: "Focus Urgent or Last" }
],
"Move": [
{ id: "movewindow l", label: "Move Window Left" },
{ id: "movewindow r", label: "Move Window Right" },
{ id: "movewindow u", label: "Move Window Up" },
{ id: "movewindow d", label: "Move Window Down" },
{ id: "movewindow", label: "Move Window (direction)" },
{ id: "swapwindow l", label: "Swap Left" },
{ id: "swapwindow r", label: "Swap Right" },
{ id: "swapwindow u", label: "Swap Up" },
{ id: "swapwindow d", label: "Swap Down" },
{ id: "swapwindow", label: "Swap Window (direction)" },
{ id: "swapnext", label: "Swap with Next" },
{ id: "swapnext prev", label: "Swap with Previous" },
{ id: "movecursortocorner", label: "Move Cursor to Corner" },
{ id: "movecursor", label: "Move Cursor (x,y)" }
],
"Workspace": [
{ id: "workspace", label: "Focus Workspace" },
{ id: "workspace +1", label: "Next Workspace" },
{ id: "workspace -1", label: "Previous Workspace" },
{ id: "workspace e+1", label: "Next Open Workspace" },
{ id: "workspace e-1", label: "Previous Open Workspace" },
{ id: "workspace previous", label: "Previous Visited Workspace" },
{ id: "workspace previous_per_monitor", label: "Previous on Monitor" },
{ id: "workspace empty", label: "First Empty Workspace" },
{ id: "movetoworkspace", label: "Move to Workspace" },
{ id: "movetoworkspace +1", label: "Move to Next Workspace" },
{ id: "movetoworkspace -1", label: "Move to Previous Workspace" },
{ id: "movetoworkspacesilent", label: "Move to Workspace (silent)" },
{ id: "movetoworkspacesilent +1", label: "Move to Next (silent)" },
{ id: "movetoworkspacesilent -1", label: "Move to Previous (silent)" },
{ id: "togglespecialworkspace", label: "Toggle Special Workspace" },
{ id: "focusworkspaceoncurrentmonitor", label: "Focus Workspace on Current Monitor" },
{ id: "renameworkspace", label: "Rename Workspace" }
],
"Monitor": [
{ id: "focusmonitor l", label: "Focus Monitor Left" },
{ id: "focusmonitor r", label: "Focus Monitor Right" },
{ id: "focusmonitor u", label: "Focus Monitor Up" },
{ id: "focusmonitor d", label: "Focus Monitor Down" },
{ id: "focusmonitor +1", label: "Focus Next Monitor" },
{ id: "focusmonitor -1", label: "Focus Previous Monitor" },
{ id: "focusmonitor", label: "Focus Monitor (by selector)" },
{ id: "movecurrentworkspacetomonitor", label: "Move Workspace to Monitor" },
{ id: "moveworkspacetomonitor", label: "Move Specific Workspace to Monitor" },
{ id: "swapactiveworkspaces", label: "Swap Active Workspaces" }
],
"Groups": [
{ id: "togglegroup", label: "Toggle Group" },
{ id: "changegroupactive f", label: "Next in Group" },
{ id: "changegroupactive b", label: "Previous in Group" },
{ id: "changegroupactive", label: "Change Active in Group" },
{ id: "moveintogroup l", label: "Move into Group Left" },
{ id: "moveintogroup r", label: "Move into Group Right" },
{ id: "moveintogroup u", label: "Move into Group Up" },
{ id: "moveintogroup d", label: "Move into Group Down" },
{ id: "moveoutofgroup", label: "Move out of Group" },
{ id: "movewindoworgroup l", label: "Move Window/Group Left" },
{ id: "movewindoworgroup r", label: "Move Window/Group Right" },
{ id: "movewindoworgroup u", label: "Move Window/Group Up" },
{ id: "movewindoworgroup d", label: "Move Window/Group Down" },
{ id: "movegroupwindow f", label: "Swap Forward in Group" },
{ id: "movegroupwindow b", label: "Swap Backward in Group" },
{ id: "lockgroups lock", label: "Lock All Groups" },
{ id: "lockgroups unlock", label: "Unlock All Groups" },
{ id: "lockgroups toggle", label: "Toggle Groups Lock" },
{ id: "lockactivegroup lock", label: "Lock Active Group" },
{ id: "lockactivegroup unlock", label: "Unlock Active Group" },
{ id: "lockactivegroup toggle", label: "Toggle Active Group Lock" },
{ id: "denywindowfromgroup on", label: "Deny Window from Group" },
{ id: "denywindowfromgroup off", label: "Allow Window in Group" },
{ id: "denywindowfromgroup toggle", label: "Toggle Deny from Group" },
{ id: "setignoregrouplock on", label: "Ignore Group Lock" },
{ id: "setignoregrouplock off", label: "Respect Group Lock" },
{ id: "setignoregrouplock toggle", label: "Toggle Ignore Group Lock" }
],
"Layout": [
{ id: "splitratio", label: "Adjust Split Ratio" }
],
"System": [
{ id: "exit", label: "Exit Hyprland" },
{ id: "forcerendererreload", label: "Force Renderer Reload" },
{ id: "dpms on", label: "DPMS On" },
{ id: "dpms off", label: "DPMS Off" },
{ id: "dpms toggle", label: "DPMS Toggle" },
{ id: "forceidle", label: "Force Idle" },
{ id: "submap", label: "Enter Submap" },
{ id: "submap reset", label: "Reset Submap" },
{ id: "global", label: "Global Shortcut" },
{ id: "event", label: "Emit Custom Event" }
],
"Pass-through": [
{ id: "pass", label: "Pass Key to Window" },
{ id: "sendshortcut", label: "Send Shortcut to Window" },
{ id: "sendkeystate", label: "Send Key State" }
]
};
const COMPOSITOR_ACTIONS = {
niri: NIRI_ACTIONS,
mangowc: MANGOWC_ACTIONS,
hyprland: HYPRLAND_ACTIONS
};
const CATEGORY_ORDER = ["DMS", "Execute", "Workspace", "Tags", "Window", "Move/Resize", "Focus", "Move", "Layout", "Groups", "Monitor", "Scratchpad", "Screenshot", "System", "Pass-through", "Overview", "Alt-Tab", "Other"];
const NIRI_ACTION_ARGS = {
"set-column-width": { "set-column-width": {
args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }] args: [{ name: "value", type: "text", label: "Width", placeholder: "+10%, -10%, 50%" }]
}, },
@@ -220,6 +457,253 @@ const ACTION_ARGS = {
} }
}; };
const MANGOWC_ACTION_ARGS = {
"view": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"tag": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"tagsilent": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"toggletag": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"toggleview": {
args: [
{ name: "tag", type: "number", label: "Tag", placeholder: "1-9" },
{ name: "monitor", type: "number", label: "Monitor", placeholder: "0", default: "0" }
]
},
"comboview": {
args: [{ name: "tags", type: "text", label: "Tags", placeholder: "1,2,3" }]
},
"setlayout": {
args: [{ name: "layout", type: "text", label: "Layout", placeholder: "tile, monocle, grid, deck" }]
},
"set_proportion": {
args: [{ name: "value", type: "text", label: "Proportion", placeholder: "0.5, +0.1, -0.1" }]
},
"setmfact": {
args: [{ name: "value", type: "text", label: "Factor", placeholder: "+0.05, -0.05" }]
},
"incgaps": {
args: [{ name: "value", type: "number", label: "Amount", placeholder: "+5, -5" }]
},
"movewin": {
args: [{ name: "value", type: "text", label: "Position", placeholder: "x,y or +10,+10" }]
},
"resizewin": {
args: [{ name: "value", type: "text", label: "Size", placeholder: "w,h or +10,+10" }]
},
"setkeymode": {
args: [{ name: "mode", type: "text", label: "Mode", placeholder: "default, custom" }]
},
"setoption": {
args: [{ name: "option", type: "text", label: "Option", placeholder: "option_name value" }]
},
"toggle_name_scratchpad": {
args: [{ name: "name", type: "text", label: "Name", placeholder: "scratchpad name" }]
},
"incnmaster": {
args: [{ name: "value", type: "number", label: "Amount", placeholder: "+1, -1" }]
}
};
const HYPRLAND_ACTION_ARGS = {
"workspace": {
args: [{ name: "value", type: "text", label: "Workspace", placeholder: "1, +1, -1, name:..." }]
},
"movetoworkspace": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, +1, special:name" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"movetoworkspacesilent": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, +1, special:name" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"focusworkspaceoncurrentmonitor": {
args: [{ name: "value", type: "text", label: "Workspace", placeholder: "1, +1, name:..." }]
},
"togglespecialworkspace": {
args: [{ name: "name", type: "text", label: "Name (optional)", placeholder: "scratchpad" }]
},
"focusmonitor": {
args: [{ name: "value", type: "text", label: "Monitor", placeholder: "l, r, +1, DP-1" }]
},
"movecurrentworkspacetomonitor": {
args: [{ name: "monitor", type: "text", label: "Monitor", placeholder: "l, r, DP-1" }]
},
"moveworkspacetomonitor": {
args: [
{ name: "workspace", type: "text", label: "Workspace", placeholder: "1, name:..." },
{ name: "monitor", type: "text", label: "Monitor", placeholder: "DP-1" }
]
},
"swapactiveworkspaces": {
args: [
{ name: "monitor1", type: "text", label: "Monitor 1", placeholder: "DP-1" },
{ name: "monitor2", type: "text", label: "Monitor 2", placeholder: "DP-2" }
]
},
"renameworkspace": {
args: [
{ name: "id", type: "number", label: "Workspace ID", placeholder: "1" },
{ name: "name", type: "text", label: "New Name", placeholder: "work" }
]
},
"fullscreen": {
args: [{ name: "mode", type: "text", label: "Mode", placeholder: "0=full, 1=max, 2=fake" }]
},
"fullscreenstate": {
args: [
{ name: "internal", type: "text", label: "Internal", placeholder: "-1, 0, 1, 2, 3" },
{ name: "client", type: "text", label: "Client", placeholder: "-1, 0, 1, 2, 3" }
]
},
"resizeactive": {
args: [{ name: "value", type: "text", label: "Size", placeholder: "10 -10, 20% 0" }]
},
"moveactive": {
args: [{ name: "value", type: "text", label: "Position", placeholder: "10 -10, exact 100 100" }]
},
"resizewindowpixel": {
args: [
{ name: "size", type: "text", label: "Size", placeholder: "100 100" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"movewindowpixel": {
args: [
{ name: "position", type: "text", label: "Position", placeholder: "100 100" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"splitratio": {
args: [{ name: "value", type: "text", label: "Ratio", placeholder: "+0.1, -0.1, exact 0.5" }]
},
"closewindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"killwindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"focuswindow": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"tagwindow": {
args: [
{ name: "tag", type: "text", label: "Tag", placeholder: "+mytag, -mytag" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"alterzorder": {
args: [
{ name: "zheight", type: "text", label: "Z-Height", placeholder: "top, bottom" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"setprop": {
args: [
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" },
{ name: "property", type: "text", label: "Property", placeholder: "opaque, alpha..." },
{ name: "value", type: "text", label: "Value", placeholder: "1, toggle" }
]
},
"signal": {
args: [{ name: "signal", type: "number", label: "Signal", placeholder: "9" }]
},
"signalwindow": {
args: [
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" },
{ name: "signal", type: "number", label: "Signal", placeholder: "9" }
]
},
"submap": {
args: [{ name: "name", type: "text", label: "Submap Name", placeholder: "resize, reset" }]
},
"global": {
args: [{ name: "name", type: "text", label: "Shortcut Name", placeholder: "app:action" }]
},
"event": {
args: [{ name: "data", type: "text", label: "Event Data", placeholder: "custom data" }]
},
"pass": {
args: [{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }]
},
"sendshortcut": {
args: [
{ name: "mod", type: "text", label: "Modifier", placeholder: "SUPER, ALT" },
{ name: "key", type: "text", label: "Key", placeholder: "F4" },
{ name: "window", type: "text", label: "Window (optional)", placeholder: "class:^(app)$" }
]
},
"sendkeystate": {
args: [
{ name: "mod", type: "text", label: "Modifier", placeholder: "SUPER" },
{ name: "key", type: "text", label: "Key", placeholder: "a" },
{ name: "state", type: "text", label: "State", placeholder: "down, repeat, up" },
{ name: "window", type: "text", label: "Window", placeholder: "class:^(app)$" }
]
},
"forceidle": {
args: [{ name: "seconds", type: "number", label: "Seconds", placeholder: "300" }]
},
"movecursortocorner": {
args: [{ name: "corner", type: "number", label: "Corner", placeholder: "0-3 (BL, BR, TR, TL)" }]
},
"movecursor": {
args: [
{ name: "x", type: "number", label: "X", placeholder: "100" },
{ name: "y", type: "number", label: "Y", placeholder: "100" }
]
},
"changegroupactive": {
args: [{ name: "direction", type: "text", label: "Direction/Index", placeholder: "f, b, or index" }]
},
"movefocus": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"movewindow": {
args: [{ name: "direction", type: "text", label: "Direction/Monitor", placeholder: "l, r, mon:DP-1" }]
},
"swapwindow": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"moveintogroup": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"movewindoworgroup": {
args: [{ name: "direction", type: "text", label: "Direction", placeholder: "l, r, u, d" }]
},
"cyclenext": {
args: [{ name: "options", type: "text", label: "Options", placeholder: "prev, tiled, floating" }]
}
};
const ACTION_ARGS = {
niri: NIRI_ACTION_ARGS,
mangowc: MANGOWC_ACTION_ARGS,
hyprland: HYPRLAND_ACTION_ARGS
};
const DMS_ACTION_ARGS = { const DMS_ACTION_ARGS = {
"audio increment": { "audio increment": {
base: "spawn dms ipc call audio increment", base: "spawn dms ipc call audio increment",
@@ -287,12 +771,18 @@ function getDmsActions(isNiri, isHyprland) {
return result; return result;
} }
function getCompositorCategories() { function getCompositorCategories(compositor) {
return Object.keys(COMPOSITOR_ACTIONS); var actions = COMPOSITOR_ACTIONS[compositor];
if (!actions)
return [];
return Object.keys(actions);
} }
function getCompositorActions(category) { function getCompositorActions(compositor, category) {
return COMPOSITOR_ACTIONS[category] || []; var actions = COMPOSITOR_ACTIONS[compositor];
if (!actions)
return [];
return actions[category] || [];
} }
function getCategoryOrder() { function getCategoryOrder() {
@@ -307,9 +797,12 @@ function findDmsAction(actionId) {
return null; return null;
} }
function findCompositorAction(actionId) { function findCompositorAction(compositor, actionId) {
for (const cat in COMPOSITOR_ACTIONS) { var actions = COMPOSITOR_ACTIONS[compositor];
const acts = COMPOSITOR_ACTIONS[cat]; if (!actions)
return null;
for (const cat in actions) {
const acts = actions[cat];
for (let i = 0; i < acts.length; i++) { for (let i = 0; i < acts.length; i++) {
if (acts[i].id === actionId) if (acts[i].id === actionId)
return acts[i]; return acts[i];
@@ -318,7 +811,7 @@ function findCompositorAction(actionId) {
return null; return null;
} }
function getActionLabel(action) { function getActionLabel(action, compositor) {
if (!action) if (!action)
return ""; return "";
@@ -326,10 +819,15 @@ function getActionLabel(action) {
if (dmsAct) if (dmsAct)
return dmsAct.label; return dmsAct.label;
var base = action.split(" ")[0]; if (compositor) {
var compAct = findCompositorAction(base); var compAct = findCompositorAction(compositor, action);
if (compAct) if (compAct)
return compAct.label; return compAct.label;
var base = action.split(" ")[0];
compAct = findCompositorAction(compositor, base);
if (compAct)
return compAct.label;
}
if (action.startsWith("spawn sh -c ")) if (action.startsWith("spawn sh -c "))
return action.slice(12).replace(/^["']|["']$/g, ""); return action.slice(12).replace(/^["']|["']$/g, "");
@@ -343,7 +841,7 @@ function getActionType(action) {
return "compositor"; return "compositor";
if (action.startsWith("spawn dms ipc call ")) if (action.startsWith("spawn dms ipc call "))
return "dms"; return "dms";
if (action.startsWith("spawn sh -c ") || action.startsWith("spawn bash -c ")) if (action.startsWith("spawn sh -c ") || action.startsWith("spawn bash -c ") || action.startsWith("spawn_shell "))
return "shell"; return "shell";
if (action.startsWith("spawn ")) if (action.startsWith("spawn "))
return "spawn"; return "spawn";
@@ -364,16 +862,21 @@ function isValidAction(action) {
case "spawn ": case "spawn ":
case "spawn sh -c \"\"": case "spawn sh -c \"\"":
case "spawn sh -c ''": case "spawn sh -c ''":
case "spawn_shell":
case "spawn_shell ":
return false; return false;
} }
return true; return true;
} }
function isKnownCompositorAction(action) { function isKnownCompositorAction(compositor, action) {
if (!action) if (!action || !compositor)
return false; return false;
var found = findCompositorAction(compositor, action);
if (found)
return true;
var base = action.split(" ")[0]; var base = action.split(" ")[0];
return findCompositorAction(base) !== null; return findCompositorAction(compositor, base) !== null;
} }
function buildSpawnAction(command, args) { function buildSpawnAction(command, args) {
@@ -385,9 +888,11 @@ function buildSpawnAction(command, args) {
return "spawn " + parts.join(" "); return "spawn " + parts.join(" ");
} }
function buildShellAction(shellCmd) { function buildShellAction(compositor, shellCmd) {
if (!shellCmd) if (!shellCmd)
return ""; return "";
if (compositor === "mangowc")
return "spawn_shell " + shellCmd;
return "spawn sh -c \"" + shellCmd.replace(/"/g, "\\\"") + "\""; return "spawn sh -c \"" + shellCmd.replace(/"/g, "\\\"") + "\"";
} }
@@ -405,21 +910,25 @@ function parseSpawnCommand(action) {
function parseShellCommand(action) { function parseShellCommand(action) {
if (!action) if (!action)
return ""; return "";
if (!action.startsWith("spawn sh -c ")) if (action.startsWith("spawn sh -c ")) {
return ""; var content = action.slice(12);
var content = action.slice(12); if ((content.startsWith('"') && content.endsWith('"')) || (content.startsWith("'") && content.endsWith("'")))
if ((content.startsWith('"') && content.endsWith('"')) || (content.startsWith("'") && content.endsWith("'"))) content = content.slice(1, -1);
content = content.slice(1, -1); return content.replace(/\\"/g, "\"");
return content.replace(/\\"/g, "\""); }
if (action.startsWith("spawn_shell "))
return action.slice(12);
return "";
} }
function getActionArgConfig(action) { function getActionArgConfig(compositor, action) {
if (!action) if (!action)
return null; return null;
var baseAction = action.split(" ")[0]; var baseAction = action.split(" ")[0];
if (ACTION_ARGS[baseAction]) var compositorArgs = ACTION_ARGS[compositor];
return { type: "compositor", base: baseAction, config: ACTION_ARGS[baseAction] }; if (compositorArgs && compositorArgs[baseAction])
return { type: "compositor", base: baseAction, config: compositorArgs[baseAction] };
for (var key in DMS_ACTION_ARGS) { for (var key in DMS_ACTION_ARGS) {
if (action.startsWith(DMS_ACTION_ARGS[key].base)) if (action.startsWith(DMS_ACTION_ARGS[key].base))
@@ -429,7 +938,7 @@ function getActionArgConfig(action) {
return null; return null;
} }
function parseCompositorActionArgs(action) { function parseCompositorActionArgs(compositor, action) {
if (!action) if (!action)
return { base: "", args: {} }; return { base: "", args: {} };
@@ -437,44 +946,144 @@ function parseCompositorActionArgs(action) {
var base = parts[0]; var base = parts[0];
var args = {}; var args = {};
if (!ACTION_ARGS[base]) var compositorArgs = ACTION_ARGS[compositor];
if (!compositorArgs || !compositorArgs[base])
return { base: action, args: {} }; return { base: action, args: {} };
var argConfig = compositorArgs[base];
var argParts = parts.slice(1); var argParts = parts.slice(1);
switch (base) { switch (compositor) {
case "move-column-to-workspace": case "niri":
for (var i = 0; i < argParts.length; i++) { switch (base) {
if (argParts[i] === "focus=true" || argParts[i] === "focus=false") { case "move-column-to-workspace":
args.focus = argParts[i] === "focus=true"; for (var i = 0; i < argParts.length; i++) {
} else if (!args.index) { if (argParts[i] === "focus=true" || argParts[i] === "focus=false") {
args.index = argParts[i]; args.focus = argParts[i] === "focus=true";
} else if (!args.index) {
args.index = argParts[i];
}
}
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
for (var k = 0; k < argParts.length; k++) {
if (argParts[k] === "focus=true" || argParts[k] === "focus=false")
args.focus = argParts[k] === "focus=true";
}
break;
default:
if (base.startsWith("screenshot")) {
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" ");
} }
} }
break; break;
case "move-column-to-workspace-down": case "mangowc":
case "move-column-to-workspace-up": if (argConfig.args && argConfig.args.length > 0 && argParts.length > 0) {
for (var k = 0; k < argParts.length; k++) { var paramStr = argParts.join(" ");
if (argParts[k] === "focus=true" || argParts[k] === "focus=false") var paramValues = paramStr.split(",");
args.focus = argParts[k] === "focus=true"; for (var m = 0; m < argConfig.args.length && m < paramValues.length; m++) {
args[argConfig.args[m].name] = paramValues[m];
}
}
break;
case "hyprland":
if (argConfig.args && argConfig.args.length > 0) {
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
var commaIdx = argParts.join(" ").indexOf(",");
if (commaIdx !== -1) {
var fullStr = argParts.join(" ");
args[argConfig.args[0].name] = fullStr.substring(0, commaIdx);
args[argConfig.args[1].name] = fullStr.substring(commaIdx + 1);
} else if (argParts.length > 0) {
args[argConfig.args[0].name] = argParts.join(" ");
}
break;
case "movetoworkspace":
case "movetoworkspacesilent":
case "tagwindow":
case "alterzorder":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts.slice(1).join(" ");
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "moveworkspacetomonitor":
case "swapactiveworkspaces":
case "renameworkspace":
case "fullscreenstate":
case "movecursor":
if (argParts.length >= 2) {
args[argConfig.args[0].name] = argParts[0];
args[argConfig.args[1].name] = argParts[1];
} else if (argParts.length === 1) {
args[argConfig.args[0].name] = argParts[0];
}
break;
case "setprop":
if (argParts.length >= 3) {
args.window = argParts[0];
args.property = argParts[1];
args.value = argParts.slice(2).join(" ");
} else if (argParts.length === 2) {
args.window = argParts[0];
args.property = argParts[1];
}
break;
case "sendshortcut":
if (argParts.length >= 3) {
args.mod = argParts[0];
args.key = argParts[1];
args.window = argParts.slice(2).join(" ");
} else if (argParts.length >= 2) {
args.mod = argParts[0];
args.key = argParts[1];
}
break;
case "sendkeystate":
if (argParts.length >= 4) {
args.mod = argParts[0];
args.key = argParts[1];
args.state = argParts[2];
args.window = argParts.slice(3).join(" ");
}
break;
case "signalwindow":
if (argParts.length >= 2) {
args.window = argParts[0];
args.signal = argParts[1];
}
break;
default:
if (argParts.length > 0) {
if (argConfig.args.length === 1) {
args[argConfig.args[0].name] = argParts.join(" ");
} else {
args.value = argParts.join(" ");
}
}
}
} }
break; break;
default: default:
if (base.startsWith("screenshot")) { if (argParts.length > 0)
for (var j = 0; j < argParts.length; j++) {
var kv = argParts[j].split("=");
if (kv.length === 2)
args[kv[0]] = kv[1] === "true";
}
} else if (argParts.length > 0) {
args.value = argParts.join(" "); args.value = argParts.join(" ");
}
} }
return { base: base, args: args }; return { base: base, args: args };
} }
function buildCompositorAction(base, args) { function buildCompositorAction(compositor, base, args) {
if (!base) if (!base)
return ""; return "";
@@ -483,29 +1092,111 @@ function buildCompositorAction(base, args) {
if (!args || Object.keys(args).length === 0) if (!args || Object.keys(args).length === 0)
return base; return base;
switch (base) { switch (compositor) {
case "move-column-to-workspace": case "niri":
if (args.index) switch (base) {
parts.push(args.index); case "move-column-to-workspace":
if (args.focus === false) if (args.index)
parts.push("focus=false"); parts.push(args.index);
if (args.focus === false)
parts.push("focus=false");
break;
case "move-column-to-workspace-down":
case "move-column-to-workspace-up":
if (args.focus === false)
parts.push("focus=false");
break;
default:
if (base.startsWith("screenshot")) {
if (args["show-pointer"] === true)
parts.push("show-pointer=true");
if (args["write-to-disk"] === true)
parts.push("write-to-disk=true");
} else if (args.value) {
parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
}
}
break; break;
case "move-column-to-workspace-down": case "mangowc":
case "move-column-to-workspace-up": var compositorArgs = ACTION_ARGS.mangowc;
if (args.focus === false) if (compositorArgs && compositorArgs[base] && compositorArgs[base].args) {
parts.push("focus=false"); var argConfig = compositorArgs[base].args;
break; var argValues = [];
default: for (var i = 0; i < argConfig.length; i++) {
if (base.startsWith("screenshot")) { var argDef = argConfig[i];
if (args["show-pointer"] === true) var val = args[argDef.name];
parts.push("show-pointer=true"); if (val === undefined || val === "")
if (args["write-to-disk"] === true) val = argDef.default || "";
parts.push("write-to-disk=true"); if (val === "" && argValues.length === 0)
continue;
argValues.push(val);
}
if (argValues.length > 0)
parts.push(argValues.join(","));
} else if (args.value) { } else if (args.value) {
parts.push(args.value); parts.push(args.value);
} else if (args.index) {
parts.push(args.index);
} }
break;
case "hyprland":
var hyprArgs = ACTION_ARGS.hyprland;
if (hyprArgs && hyprArgs[base] && hyprArgs[base].args) {
var hyprConfig = hyprArgs[base].args;
switch (base) {
case "resizewindowpixel":
case "movewindowpixel":
if (args[hyprConfig[0].name])
parts.push(args[hyprConfig[0].name]);
if (args[hyprConfig[1].name])
parts[parts.length - 1] += "," + args[hyprConfig[1].name];
break;
case "setprop":
if (args.window)
parts.push(args.window);
if (args.property)
parts.push(args.property);
if (args.value)
parts.push(args.value);
break;
case "sendshortcut":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.window)
parts.push(args.window);
break;
case "sendkeystate":
if (args.mod)
parts.push(args.mod);
if (args.key)
parts.push(args.key);
if (args.state)
parts.push(args.state);
if (args.window)
parts.push(args.window);
break;
case "signalwindow":
if (args.window)
parts.push(args.window);
if (args.signal)
parts.push(args.signal);
break;
default:
for (var j = 0; j < hyprConfig.length; j++) {
var hVal = args[hyprConfig[j].name];
if (hVal !== undefined && hVal !== "")
parts.push(hVal);
}
}
} else if (args.value) {
parts.push(args.value);
}
break;
default:
if (args.value)
parts.push(args.value);
} }
return parts.join(" "); return parts.join(" ");

View File

@@ -189,6 +189,13 @@ Item {
if (CompositorService.isNiri && NiriService.currentOutput) { if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput; return NiriService.currentOutput;
} }
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || "";
}
if (CompositorService.isDwl && DwlService.activeOutput) {
return DwlService.activeOutput;
}
return ""; return "";
} }

View File

@@ -99,6 +99,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {
@@ -126,6 +128,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} }
if (!focusedScreenName && barVariants.instances.length > 0) { if (!focusedScreenName && barVariants.instances.length > 0) {

View File

@@ -136,12 +136,12 @@ Item {
} }
} }
function _ensureNiriProvider() { function _ensureCurrentProvider() {
if (!KeybindsService.available) if (!KeybindsService.available)
return; return;
const cachedProvider = KeybindsService.keybinds?.provider; const cachedProvider = KeybindsService.keybinds?.provider;
if (cachedProvider !== "niri" || KeybindsService._dataVersion === 0) { const targetProvider = KeybindsService.currentProvider;
KeybindsService.currentProvider = "niri"; if (cachedProvider !== targetProvider || KeybindsService._dataVersion === 0) {
KeybindsService.loadBinds(); KeybindsService.loadBinds();
return; return;
} }
@@ -152,13 +152,13 @@ Item {
} }
} }
Component.onCompleted: _ensureNiriProvider() Component.onCompleted: _ensureCurrentProvider()
onVisibleChanged: { onVisibleChanged: {
if (!visible) if (!visible)
return; return;
Qt.callLater(scrollToTop); Qt.callLater(scrollToTop);
_ensureNiriProvider(); _ensureCurrentProvider();
} }
DankFlickable { DankFlickable {
@@ -213,7 +213,8 @@ Item {
} }
StyledText { StyledText {
text: I18n.tr("Click any shortcut to edit. Changes save to dms/binds.kdl") readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
text: I18n.tr("Click any shortcut to edit. Changes save to %1").arg(bindsFile)
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -310,11 +311,12 @@ Item {
} }
StyledText { StyledText {
readonly property string bindsFile: KeybindsService.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf"
text: { text: {
if (warningBox.showSetup) if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create dms/binds.kdl and add include to config.kdl."); return I18n.tr("Click 'Setup' to create %1 and add include to config.").arg(bindsFile);
if (warningBox.showError) if (warningBox.showError)
return I18n.tr("dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed."); return I18n.tr("%1 exists but is not included in config. Custom keybinds will not work until this is fixed.").arg(bindsFile);
if (warningBox.showWarning) { if (warningBox.showWarning) {
const count = warningBox.status.overriddenBy; const count = warningBox.status.overriddenBy;
return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count); return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count);

View File

@@ -352,6 +352,14 @@ FocusScope {
} }
refreshPluginList(); refreshPluginList();
} }
function onPluginDataChanged(pluginId) {
var plugin = PluginService.availablePlugins[pluginId];
if (!plugin || !PluginService.isPluginLoaded(pluginId))
return;
var isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
if (isLauncher)
PluginService.reloadPlugin(pluginId);
}
} }
Connections { Connections {

View File

@@ -1,11 +1,12 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland // ! Even though qmlls says this is unused, it is wrong import Quickshell.Wayland
// ! Even though qmlls says this is unused, it is wrong
import qs.Common import qs.Common
import "../Common/KeybindActions.js" as Actions import "../Common/KeybindActions.js" as Actions
@@ -26,14 +27,24 @@ Singleton {
} }
} }
property bool available: CompositorService.isNiri && shortcutInhibitorAvailable property bool available: (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl) && shortcutInhibitorAvailable
property string currentProvider: "niri" property string currentProvider: {
if (CompositorService.isNiri)
return "niri";
if (CompositorService.isHyprland)
return "hyprland";
if (CompositorService.isDwl)
return "mangowc";
return "";
}
readonly property string cheatsheetProvider: { readonly property string cheatsheetProvider: {
if (CompositorService.isNiri) if (CompositorService.isNiri)
return "niri"; return "niri";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return "hyprland"; return "hyprland";
if (CompositorService.isDwl)
return "mangowc";
return ""; return "";
} }
property bool cheatsheetAvailable: cheatsheetProvider !== "" property bool cheatsheetAvailable: cheatsheetProvider !== ""
@@ -47,14 +58,14 @@ Singleton {
property bool dmsBindsIncluded: true property bool dmsBindsIncluded: true
property var dmsStatus: ({ property var dmsStatus: ({
exists: true, "exists": true,
included: true, "included": true,
includePosition: -1, "includePosition": -1,
totalIncludes: 0, "totalIncludes": 0,
bindsAfterDms: 0, "bindsAfterDms": 0,
effective: true, "effective": true,
overriddenBy: 0, "overriddenBy": 0,
statusMessage: "" "statusMessage": ""
}) })
property var _rawData: null property var _rawData: null
@@ -67,7 +78,41 @@ Singleton {
readonly property var categoryOrder: Actions.getCategoryOrder() readonly property var categoryOrder: Actions.getCategoryOrder()
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string dmsBindsPath: configDir + "/niri/dms/binds.kdl" readonly property string compositorConfigDir: {
switch (currentProvider) {
case "niri":
return configDir + "/niri";
case "hyprland":
return configDir + "/hypr";
case "mangowc":
return configDir + "/mango";
default:
return "";
}
}
readonly property string dmsBindsPath: {
switch (currentProvider) {
case "niri":
return compositorConfigDir + "/dms/binds.kdl";
case "hyprland":
case "mangowc":
return compositorConfigDir + "/dms/binds.conf";
default:
return "";
}
}
readonly property string mainConfigPath: {
switch (currentProvider) {
case "niri":
return compositorConfigDir + "/config.kdl";
case "hyprland":
return compositorConfigDir + "/hyprland.conf";
case "mangowc":
return compositorConfigDir + "/config.conf";
default:
return "";
}
}
readonly property var actionTypes: Actions.getActionTypes() readonly property var actionTypes: Actions.getActionTypes()
readonly property var dmsActions: getDmsActions() readonly property var dmsActions: getDmsActions()
@@ -215,19 +260,33 @@ Singleton {
root.lastError = ""; root.lastError = "";
root.dmsBindsIncluded = true; root.dmsBindsIncluded = true;
root.dmsBindsFixed(); root.dmsBindsFixed();
ToastService.showSuccess(I18n.tr("Binds include added"), I18n.tr("dms/binds.kdl is now included in config.kdl"), "", "keybinds"); const bindsFile = root.currentProvider === "niri" ? "dms/binds.kdl" : "dms/binds.conf";
ToastService.showInfo(I18n.tr("Binds include added"), I18n.tr("%1 is now included in config").arg(bindsFile), "", "keybinds");
Qt.callLater(root.forceReload); Qt.callLater(root.forceReload);
} }
} }
function fixDmsBindsInclude() { function fixDmsBindsInclude() {
if (fixing || dmsBindsIncluded) if (fixing || dmsBindsIncluded || !compositorConfigDir)
return; return;
fixing = true; fixing = true;
const niriConfigDir = configDir + "/niri";
const timestamp = Math.floor(Date.now() / 1000); const timestamp = Math.floor(Date.now() / 1000);
const backupPath = `${niriConfigDir}/config.kdl.dmsbackup${timestamp}`; const backupPath = `${mainConfigPath}.dmsbackup${timestamp}`;
const script = `mkdir -p "${niriConfigDir}/dms" && touch "${niriConfigDir}/dms/binds.kdl" && cp "${niriConfigDir}/config.kdl" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${niriConfigDir}/config.kdl"`; let script;
switch (currentProvider) {
case "niri":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.kdl" && cp "${mainConfigPath}" "${backupPath}" && echo 'include "dms/binds.kdl"' >> "${mainConfigPath}"`;
break;
case "hyprland":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = dms/binds.conf' >> "${mainConfigPath}"`;
break;
case "mangowc":
script = `mkdir -p "${compositorConfigDir}/dms" && touch "${compositorConfigDir}/dms/binds.conf" && cp "${mainConfigPath}" "${backupPath}" && echo 'source = ./dms/binds.conf' >> "${mainConfigPath}"`;
break;
default:
fixing = false;
return;
}
fixProcess.command = ["sh", "-c", script]; fixProcess.command = ["sh", "-c", script];
fixProcess.running = true; fixProcess.running = true;
} }
@@ -261,21 +320,19 @@ Singleton {
function _processData() { function _processData() {
keybinds = _rawData || {}; keybinds = _rawData || {};
if (currentProvider === "niri") { dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true;
dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true; const status = _rawData?.dmsStatus;
const status = _rawData?.dmsStatus; if (status) {
if (status) { dmsStatus = {
dmsStatus = { "exists": status.exists ?? true,
exists: status.exists ?? true, "included": status.included ?? true,
included: status.included ?? true, "includePosition": status.includePosition ?? -1,
includePosition: status.includePosition ?? -1, "totalIncludes": status.totalIncludes ?? 0,
totalIncludes: status.totalIncludes ?? 0, "bindsAfterDms": status.bindsAfterDms ?? 0,
bindsAfterDms: status.bindsAfterDms ?? 0, "effective": status.effective ?? true,
effective: status.effective ?? true, "overriddenBy": status.overriddenBy ?? 0,
overriddenBy: status.overriddenBy ?? 0, "statusMessage": status.statusMessage ?? ""
statusMessage: status.statusMessage ?? "" };
};
}
} }
if (!_rawData?.binds) { if (!_rawData?.binds) {
@@ -292,7 +349,7 @@ Singleton {
const bindsData = _rawData.binds; const bindsData = _rawData.binds;
for (const cat in bindsData) { for (const cat in bindsData) {
const binds = bindsData[cat]; const binds = bindsData[cat];
for (let i = 0; i < binds.length; i++) { for (var i = 0; i < binds.length; i++) {
const bind = binds[i]; const bind = binds[i];
const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat; const targetCat = Actions.isDmsAction(bind.action) ? "DMS" : cat;
if (!processed[targetCat]) if (!processed[targetCat])
@@ -309,19 +366,19 @@ Singleton {
const grouped = []; const grouped = [];
const actionMap = {}; const actionMap = {};
for (let ci = 0; ci < sortedCats.length; ci++) { for (var ci = 0; ci < sortedCats.length; ci++) {
const category = sortedCats[ci]; const category = sortedCats[ci];
const binds = processed[category]; const binds = processed[category];
if (!binds) if (!binds)
continue; continue;
for (let i = 0; i < binds.length; i++) { for (var i = 0; i < binds.length; i++) {
const bind = binds[i]; const bind = binds[i];
const action = bind.action || ""; const action = bind.action || "";
const keyData = { const keyData = {
key: bind.key || "", "key": bind.key || "",
source: bind.source || "config", "source": bind.source || "config",
isOverride: bind.source === "dms", "isOverride": bind.source === "dms",
cooldownMs: bind.cooldownMs || 0 "cooldownMs": bind.cooldownMs || 0
}; };
if (actionMap[action]) { if (actionMap[action]) {
actionMap[action].keys.push(keyData); actionMap[action].keys.push(keyData);
@@ -331,11 +388,11 @@ Singleton {
actionMap[action].conflict = bind.conflict; actionMap[action].conflict = bind.conflict;
} else { } else {
const entry = { const entry = {
category: category, "category": category,
action: action, "action": action,
desc: bind.desc || "", "desc": bind.desc || "",
keys: [keyData], "keys": [keyData],
conflict: bind.conflict || null "conflict": bind.conflict || null
}; };
actionMap[action] = entry; actionMap[action] = entry;
grouped.push(entry); grouped.push(entry);
@@ -346,19 +403,19 @@ Singleton {
const list = []; const list = [];
for (const cat of sortedCats) { for (const cat of sortedCats) {
list.push({ list.push({
id: "cat:" + cat, "id": "cat:" + cat,
type: "category", "type": "category",
name: cat "name": cat
}); });
const binds = processed[cat]; const binds = processed[cat];
if (!binds) if (!binds)
continue; continue;
for (const bind of binds) for (const bind of binds)
list.push({ list.push({
id: "bind:" + bind.key, "id": "bind:" + bind.key,
type: "bind", "type": "bind",
key: bind.key, "key": bind.key,
desc: bind.desc "desc": bind.desc
}); });
} }
@@ -413,15 +470,15 @@ Singleton {
} }
function getActionLabel(action) { function getActionLabel(action) {
return Actions.getActionLabel(action); return Actions.getActionLabel(action, currentProvider);
} }
function getCompositorCategories() { function getCompositorCategories() {
return Actions.getCompositorCategories(); return Actions.getCompositorCategories(currentProvider);
} }
function getCompositorActions(category) { function getCompositorActions(category) {
return Actions.getCompositorActions(category); return Actions.getCompositorActions(currentProvider, category);
} }
function getDmsActions() { function getDmsActions() {
@@ -433,7 +490,7 @@ Singleton {
} }
function buildShellAction(shellCmd) { function buildShellAction(shellCmd) {
return Actions.buildShellAction(shellCmd); return Actions.buildShellAction(currentProvider, shellCmd);
} }
function parseSpawnCommand(action) { function parseSpawnCommand(action) {

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -42,7 +42,7 @@ Item {
readonly property var keys: bindData.keys || [] readonly property var keys: bindData.keys || []
readonly property bool hasOverride: { readonly property bool hasOverride: {
for (let i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (keys[i].isOverride) if (keys[i].isOverride)
return true; return true;
} }
@@ -92,7 +92,7 @@ Item {
} }
function restoreToKey(keyToFind) { function restoreToKey(keyToFind) {
for (let i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (keys[i].key === keyToFind) { if (keys[i].key === keyToFind) {
editingKeyIndex = i; editingKeyIndex = i;
editKey = keyToFind; editKey = keyToFind;
@@ -106,7 +106,7 @@ Item {
} }
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction); useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
return; return;
} }
} }
@@ -126,7 +126,7 @@ Item {
editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0; editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
hasChanges = false; hasChanges = false;
_actionType = Actions.getActionType(editAction); _actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(editAction); useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
} }
function startAddingNewKey() { function startAddingNewKey() {
@@ -177,10 +177,10 @@ Item {
desc = expandedLoader.item.currentTitle; desc = expandedLoader.item.currentTitle;
_savedCooldownMs = editCooldownMs; _savedCooldownMs = editCooldownMs;
saveBind(origKey, { saveBind(origKey, {
key: editKey, "key": editKey,
action: editAction, "action": editAction,
desc: desc, "desc": desc,
cooldownMs: editCooldownMs "cooldownMs": editCooldownMs
}); });
hasChanges = false; hasChanges = false;
addingNewKey = false; addingNewKey = false;
@@ -189,15 +189,14 @@ Item {
function _createShortcutInhibitor() { function _createShortcutInhibitor() {
if (!_shortcutInhibitorAvailable || _shortcutInhibitor) if (!_shortcutInhibitorAvailable || _shortcutInhibitor)
return; return;
const qmlString = ` const qmlString = `
import QtQuick import QtQuick
import Quickshell.Wayland import Quickshell.Wayland
ShortcutInhibitor { ShortcutInhibitor {
enabled: false enabled: false
window: null window: null
} }
`; `;
_shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor"); _shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor");
@@ -207,18 +206,21 @@ Item {
function _destroyShortcutInhibitor() { function _destroyShortcutInhibitor() {
if (_shortcutInhibitor) { if (_shortcutInhibitor) {
_shortcutInhibitor.enabled = false;
_shortcutInhibitor.destroy(); _shortcutInhibitor.destroy();
_shortcutInhibitor = null; _shortcutInhibitor = null;
} }
} }
function startRecording() { function startRecording() {
_destroyShortcutInhibitor();
_createShortcutInhibitor(); _createShortcutInhibitor();
recording = true; recording = true;
} }
function stopRecording() { function stopRecording() {
recording = false; recording = false;
_destroyShortcutInhibitor();
} }
Column { Column {
@@ -617,7 +619,6 @@ Item {
Keys.onPressed: event => { Keys.onPressed: event => {
if (!root.recording) if (!root.recording)
return; return;
event.accepted = true; event.accepted = true;
switch (event.key) { switch (event.key) {
@@ -654,7 +655,7 @@ Item {
} }
root.updateEdit({ root.updateEdit({
key: KeyUtils.formatToken(mods, key) "key": KeyUtils.formatToken(mods, key)
}); });
root.stopRecording(); root.stopRecording();
} }
@@ -699,9 +700,8 @@ Item {
if (!wheelKey) if (!wheelKey)
return; return;
root.updateEdit({ root.updateEdit({
key: KeyUtils.formatToken(mods, wheelKey) "key": KeyUtils.formatToken(mods, wheelKey)
}); });
root.stopRecording(); root.stopRecording();
} }
@@ -824,26 +824,26 @@ Item {
switch (typeDelegate.modelData.id) { switch (typeDelegate.modelData.id) {
case "dms": case "dms":
root.updateEdit({ root.updateEdit({
action: KeybindsService.dmsActions[0].id, "action": KeybindsService.dmsActions[0].id,
desc: KeybindsService.dmsActions[0].label "desc": KeybindsService.dmsActions[0].label
}); });
break; break;
case "compositor": case "compositor":
root.updateEdit({ root.updateEdit({
action: "close-window", "action": "close-window",
desc: "Close Window" "desc": "Close Window"
}); });
break; break;
case "spawn": case "spawn":
root.updateEdit({ root.updateEdit({
action: "spawn ", "action": "spawn ",
desc: "" "desc": ""
}); });
break; break;
case "shell": case "shell":
root.updateEdit({ root.updateEdit({
action: "spawn sh -c \"\"", "action": "spawn sh -c \"\"",
desc: "" "desc": ""
}); });
break; break;
} }
@@ -890,8 +890,8 @@ Item {
for (const act of actions) { for (const act of actions) {
if (act.label === value) { if (act.label === value) {
root.updateEdit({ root.updateEdit({
action: act.id, "action": act.id,
desc: act.label "desc": act.label
}); });
return; return;
} }
@@ -905,12 +905,12 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM
readonly property var argConfig: Actions.getActionArgConfig(root.editAction) readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
readonly property var parsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(root.editAction) : null readonly property var parsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(root.editAction) : null
readonly property var dmsActionArgs: Actions.getDmsActionArgs() readonly property var dmsActionArgs: Actions.getDmsActionArgs()
readonly property bool hasAmountArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "amount") ?? false) : false readonly property bool hasAmountArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "amount") ?? false) : false
readonly property bool hasDeviceArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "device") ?? false) : false readonly property bool hasDeviceArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "device") ?? false) : false
readonly property bool hasTabArg: parsedArgs?.base ? (dmsActionArgs?.[parsedArgs.base]?.args?.some(a => a.name === "tab") ?? false) : false readonly property bool hasTabArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "tab") ?? false) : false
visible: root._actionType === "dms" && argConfig?.type === "dms" visible: root._actionType === "dms" && argConfig?.type === "dms"
@@ -949,7 +949,7 @@ Item {
const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args); const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
newArgs.amount = text || "5"; newArgs.amount = text || "5";
root.updateEdit({ root.updateEdit({
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs) "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
}); });
} }
} }
@@ -997,7 +997,7 @@ Item {
const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args); const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
newArgs.device = text; newArgs.device = text;
root.updateEdit({ root.updateEdit({
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs) "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
}); });
} }
} }
@@ -1054,7 +1054,7 @@ Item {
break; break;
} }
root.updateEdit({ root.updateEdit({
action: Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs) "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
}); });
} }
} }
@@ -1104,8 +1104,8 @@ Item {
for (const act of actions) { for (const act of actions) {
if (act.label === value) { if (act.label === value) {
root.updateEdit({ root.updateEdit({
action: act.id, "action": act.id,
desc: act.label "desc": act.label
}); });
return; return;
} }
@@ -1146,10 +1146,10 @@ Item {
id: optionsRow id: optionsRow
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM
visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(root.editAction) visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
readonly property var argConfig: Actions.getActionArgConfig(root.editAction) readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
readonly property var parsedArgs: Actions.parseCompositorActionArgs(root.editAction) readonly property var parsedArgs: Actions.parseCompositorActionArgs(KeybindsService.currentProvider, root.editAction)
StyledText { StyledText {
text: I18n.tr("Options") text: I18n.tr("Options")
@@ -1167,6 +1167,9 @@ Item {
id: argValueField id: argValueField
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: root._inputHeight Layout.preferredHeight: root._inputHeight
readonly property string _argName: optionsRow.argConfig?.config?.args[0]?.name || "value"
visible: { visible: {
const cfg = optionsRow.argConfig; const cfg = optionsRow.argConfig;
if (!cfg?.config?.args) if (!cfg?.config?.args)
@@ -1174,19 +1177,20 @@ Item {
const firstArg = cfg.config.args[0]; const firstArg = cfg.config.args[0];
return firstArg && (firstArg.type === "text" || firstArg.type === "number"); return firstArg && (firstArg.type === "text" || firstArg.type === "number");
} }
placeholderText: optionsRow.argConfig?.config?.args?.[0]?.placeholder || "" placeholderText: optionsRow.argConfig?.config?.args[0]?.placeholder || ""
Connections { Connections {
target: optionsRow target: optionsRow
function onParsedArgsChanged() { function onParsedArgsChanged() {
const newText = optionsRow.parsedArgs?.args?.value || optionsRow.parsedArgs?.args?.index || ""; const argName = argValueField._argName;
const newText = optionsRow.parsedArgs?.args[argName] || "";
if (argValueField.text !== newText) if (argValueField.text !== newText)
argValueField.text = newText; argValueField.text = newText;
} }
} }
Component.onCompleted: { Component.onCompleted: {
text = optionsRow.parsedArgs?.args?.value || optionsRow.parsedArgs?.args?.index || ""; text = optionsRow.parsedArgs?.args[_argName] || "";
} }
onEditingFinished: { onEditingFinished: {
@@ -1194,15 +1198,10 @@ Item {
if (!cfg) if (!cfg)
return; return;
const parsed = optionsRow.parsedArgs; const parsed = optionsRow.parsedArgs;
const args = {}; const args = Object.assign({}, parsed?.args || {});
if (cfg.config.args[0]?.type === "number") args[_argName] = text;
args.index = text;
else
args.value = text;
if (parsed?.args?.focus === false)
args.focus = false;
root.updateEdit({ root.updateEdit({
action: Actions.buildCompositorAction(parsed?.base || cfg.base, args) "action": Actions.buildCompositorAction(KeybindsService.currentProvider, parsed?.base || cfg.base, args)
}); });
} }
} }
@@ -1236,7 +1235,7 @@ Item {
if (!newChecked) if (!newChecked)
args.focus = false; args.focus = false;
root.updateEdit({ root.updateEdit({
action: Actions.buildCompositorAction(cfg.base, args) "action": Actions.buildCompositorAction(KeybindsService.currentProvider, cfg.base, args)
}); });
} }
} }
@@ -1257,14 +1256,14 @@ Item {
DankToggle { DankToggle {
id: showPointerToggle id: showPointerToggle
checked: optionsRow.parsedArgs?.args?.["show-pointer"] === true checked: optionsRow.parsedArgs?.args["show-pointer"] === true
onToggled: newChecked => { onToggled: newChecked => {
const parsed = optionsRow.parsedArgs; const parsed = optionsRow.parsedArgs;
const base = parsed?.base || "screenshot"; const base = parsed?.base || "screenshot";
const args = Object.assign({}, parsed?.args || {}); const args = Object.assign({}, parsed?.args || {});
args["show-pointer"] = newChecked; args["show-pointer"] = newChecked;
root.updateEdit({ root.updateEdit({
action: Actions.buildCompositorAction(base, args) "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
}); });
} }
} }
@@ -1282,14 +1281,14 @@ Item {
DankToggle { DankToggle {
id: writeToDiskToggle id: writeToDiskToggle
checked: optionsRow.parsedArgs?.args?.["write-to-disk"] === true checked: optionsRow.parsedArgs?.args["write-to-disk"] === true
onToggled: newChecked => { onToggled: newChecked => {
const parsed = optionsRow.parsedArgs; const parsed = optionsRow.parsedArgs;
const base = parsed?.base || "screenshot-screen"; const base = parsed?.base || "screenshot-screen";
const args = Object.assign({}, parsed?.args || {}); const args = Object.assign({}, parsed?.args || {});
args["write-to-disk"] = newChecked; args["write-to-disk"] = newChecked;
root.updateEdit({ root.updateEdit({
action: Actions.buildCompositorAction(base, args) "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
}); });
} }
} }
@@ -1327,7 +1326,7 @@ Item {
if (root._actionType !== "compositor") if (root._actionType !== "compositor")
return; return;
root.updateEdit({ root.updateEdit({
action: text "action": text
}); });
} }
} }
@@ -1359,8 +1358,8 @@ Item {
onClicked: { onClicked: {
root.useCustomCompositor = false; root.useCustomCompositor = false;
root.updateEdit({ root.updateEdit({
action: "close-window", "action": "close-window",
desc: "Close Window" "desc": "Close Window"
}); });
} }
} }
@@ -1393,7 +1392,7 @@ Item {
const parts = text.trim().split(" ").filter(p => p); const parts = text.trim().split(" ").filter(p => p);
const action = parts.length > 0 ? "spawn " + parts.join(" ") : "spawn "; const action = parts.length > 0 ? "spawn " + parts.join(" ") : "spawn ";
root.updateEdit({ root.updateEdit({
action: action "action": action
}); });
} }
} }
@@ -1422,7 +1421,7 @@ Item {
if (root._actionType !== "shell") if (root._actionType !== "shell")
return; return;
root.updateEdit({ root.updateEdit({
action: Actions.buildShellAction(text) "action": Actions.buildShellAction(KeybindsService.currentProvider, text)
}); });
} }
} }
@@ -1447,7 +1446,7 @@ Item {
placeholderText: I18n.tr("Hotkey overlay title (optional)") placeholderText: I18n.tr("Hotkey overlay title (optional)")
text: root.editDesc text: root.editDesc
onTextChanged: root.updateEdit({ onTextChanged: root.updateEdit({
desc: text "desc": text
}) })
} }
} }
@@ -1455,6 +1454,7 @@ Item {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM
visible: KeybindsService.currentProvider === "niri"
StyledText { StyledText {
text: I18n.tr("Cooldown") text: I18n.tr("Cooldown")
@@ -1487,7 +1487,7 @@ Item {
const val = parseInt(text) || 0; const val = parseInt(text) || 0;
if (val !== root.editCooldownMs) if (val !== root.editCooldownMs)
root.updateEdit({ root.updateEdit({
cooldownMs: val "cooldownMs": val
}); });
} }
} }