1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

keybinds: always parse binds.kdl, show warning on position-conflicts

This commit is contained in:
bbedward
2025-12-03 10:31:55 -05:00
parent 33e655becd
commit 5f5427266f
7 changed files with 450 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -53,14 +54,29 @@ func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
n.parsed = true n.parsed = true
categorizedBinds := make(map[string][]keybinds.Keybind) categorizedBinds := make(map[string][]keybinds.Keybind)
n.convertSection(result.Section, "", categorizedBinds) n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs)
return &keybinds.CheatSheet{ sheet := &keybinds.CheatSheet{
Title: "Niri Keybinds", Title: "Niri Keybinds",
Provider: n.Name(), Provider: n.Name(),
Binds: categorizedBinds, Binds: categorizedBinds,
DMSBindsIncluded: result.DMSBindsIncluded, DMSBindsIncluded: result.DMSBindsIncluded,
}, nil }
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 (n *NiriProvider) HasDMSBindsIncluded() bool { func (n *NiriProvider) HasDMSBindsIncluded() bool {
@@ -78,7 +94,7 @@ func (n *NiriProvider) HasDMSBindsIncluded() bool {
return n.dmsBindsIncluded return n.dmsBindsIncluded
} }
func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) {
currentSubcat := subcategory currentSubcat := subcategory
if section.Name != "" { if section.Name != "" {
currentSubcat = section.Name currentSubcat = section.Name
@@ -86,12 +102,12 @@ func (n *NiriProvider) convertSection(section *NiriSection, subcategory string,
for _, kb := range section.Keybinds { for _, kb := range section.Keybinds {
category := n.categorizeByAction(kb.Action) category := n.categorizeByAction(kb.Action)
bind := n.convertKeybind(&kb, currentSubcat) bind := n.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 {
n.convertSection(&child, currentSubcat, categorizedBinds) n.convertSection(&child, currentSubcat, categorizedBinds, conflicts)
} }
} }
@@ -128,21 +144,35 @@ func (n *NiriProvider) categorizeByAction(action string) string {
} }
} }
func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string) keybinds.Keybind { func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind {
rawAction := n.formatRawAction(kb.Action, kb.Args) rawAction := n.formatRawAction(kb.Action, kb.Args)
keyStr := n.formatKey(kb)
source := "config" source := "config"
if strings.Contains(kb.Source, "dms/binds.kdl") { if strings.Contains(kb.Source, "dms/binds.kdl") {
source = "dms" source = "dms"
} }
return keybinds.Keybind{ bind := keybinds.Keybind{
Key: n.formatKey(kb), Key: keyStr,
Description: kb.Description, Description: kb.Description,
Action: rawAction, Action: rawAction,
Subcategory: subcategory, Subcategory: subcategory,
Source: source, Source: source,
} }
if source == "dms" && conflicts != nil {
if conflictKb, ok := conflicts[keyStr]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Description,
Action: n.formatRawAction(conflictKb.Action, conflictKb.Args),
Source: "config",
}
}
}
return bind
} }
func (n *NiriProvider) formatRawAction(action string, args []string) string { func (n *NiriProvider) formatRawAction(action string, args []string) string {
@@ -386,6 +416,29 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
return os.WriteFile(overridePath, []byte(content), 0644) return os.WriteFile(overridePath, []byte(content), 0644)
} }
func (n *NiriProvider) getBindSortPriority(action string) int {
switch {
case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"):
return 0
case strings.Contains(action, "workspace"):
return 1
case strings.Contains(action, "window") || strings.Contains(action, "column") ||
strings.Contains(action, "focus") || strings.Contains(action, "move") ||
strings.Contains(action, "swap") || strings.Contains(action, "resize"):
return 2
case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"):
return 3
case strings.Contains(action, "screenshot"):
return 4
case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"):
return 5
case strings.HasPrefix(action, "spawn"):
return 6
default:
return 7
}
}
func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string {
if len(binds) == 0 { if len(binds) == 0 {
return "binds {}\n" return "binds {}\n"
@@ -401,6 +454,18 @@ func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) stri
} }
} }
sort.Slice(regularBinds, func(i, j int) bool {
pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action)
if pi != pj {
return pi < pj
}
return regularBinds[i].Key < regularBinds[j].Key
})
sort.Slice(recentWindowsBinds, func(i, j int) bool {
return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key
})
var sb strings.Builder var sb strings.Builder
sb.WriteString("binds {\n") sb.WriteString("binds {\n")

View File

@@ -26,35 +26,78 @@ type NiriSection struct {
} }
type NiriParser struct { type NiriParser struct {
configDir string configDir string
processedFiles map[string]bool processedFiles map[string]bool
bindMap map[string]*NiriKeyBinding bindMap map[string]*NiriKeyBinding
bindOrder []string bindOrder []string
currentSource string currentSource string
dmsBindsIncluded bool dmsBindsIncluded bool
dmsBindsExists bool
includeCount int
dmsIncludePos int
bindsBeforeDMS int
bindsAfterDMS int
dmsBindKeys map[string]bool
configBindKeys map[string]bool
dmsProcessed bool
dmsBindMap map[string]*NiriKeyBinding
conflictingConfigs map[string]*NiriKeyBinding
} }
func NewNiriParser(configDir string) *NiriParser { func NewNiriParser(configDir string) *NiriParser {
return &NiriParser{ return &NiriParser{
configDir: configDir, configDir: configDir,
processedFiles: make(map[string]bool), processedFiles: make(map[string]bool),
bindMap: make(map[string]*NiriKeyBinding), bindMap: make(map[string]*NiriKeyBinding),
bindOrder: []string{}, bindOrder: []string{},
currentSource: "", currentSource: "",
dmsIncludePos: -1,
dmsBindKeys: make(map[string]bool),
configBindKeys: make(map[string]bool),
dmsBindMap: make(map[string]*NiriKeyBinding),
conflictingConfigs: make(map[string]*NiriKeyBinding),
} }
} }
func (p *NiriParser) Parse() (*NiriSection, error) { func (p *NiriParser) Parse() (*NiriSection, error) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil {
p.dmsBindsExists = true
}
configPath := filepath.Join(p.configDir, "config.kdl") configPath := filepath.Join(p.configDir, "config.kdl")
section, err := p.parseFile(configPath, "") section, err := p.parseFile(configPath, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if p.dmsBindsExists && !p.dmsProcessed {
p.parseDMSBindsDirectly(dmsBindsPath, section)
}
section.Keybinds = p.finalizeBinds() section.Keybinds = p.finalizeBinds()
return section, nil return section, nil
} }
func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) {
data, err := os.ReadFile(dmsBindsPath)
if err != nil {
return
}
doc, err := kdl.Parse(strings.NewReader(string(data)))
if err != nil {
return
}
prevSource := p.currentSource
p.currentSource = dmsBindsPath
baseDir := filepath.Dir(dmsBindsPath)
p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
p.dmsProcessed = true
}
func (p *NiriParser) finalizeBinds() []NiriKeyBinding { func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
binds := make([]NiriKeyBinding, 0, len(p.bindOrder)) binds := make([]NiriKeyBinding, 0, len(p.bindOrder))
for _, key := range p.bindOrder { for _, key := range p.bindOrder {
@@ -67,6 +110,20 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) { func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb) key := p.formatBindKey(kb)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind {
p.dmsBindKeys[key] = true
p.dmsBindMap[key] = kb
} else if p.dmsBindKeys[key] {
p.bindsAfterDMS++
p.conflictingConfigs[key] = kb
p.configBindKeys[key] = true
return
} else {
p.configBindKeys[key] = true
}
if _, exists := p.bindMap[key]; !exists { if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key) p.bindOrder = append(p.bindOrder, key)
} }
@@ -105,9 +162,11 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
Name: sectionName, Name: sectionName,
} }
prevSource := p.currentSource
p.currentSource = absPath p.currentSource = absPath
baseDir := filepath.Dir(absPath) baseDir := filepath.Dir(absPath)
p.processNodes(doc.Nodes, section, baseDir) p.processNodes(doc.Nodes, section, baseDir)
p.currentSource = prevSource
return section, nil return section, nil
} }
@@ -133,8 +192,13 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
} }
includePath := strings.Trim(node.Arguments[0].String(), "\"") includePath := strings.Trim(node.Arguments[0].String(), "\"")
if includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") { isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl")
p.includeCount++
if isDMSInclude {
p.dmsBindsIncluded = true p.dmsBindsIncluded = true
p.dmsIncludePos = p.includeCount
p.bindsBeforeDMS = len(p.bindMap)
} }
fullPath := filepath.Join(baseDir, includePath) fullPath := filepath.Join(baseDir, includePath)
@@ -142,6 +206,10 @@ func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, ba
fullPath = includePath fullPath = includePath
} }
if isDMSInclude {
p.dmsProcessed = true
}
includedSection, err := p.parseFile(fullPath, "") includedSection, err := p.parseFile(fullPath, "")
if err != nil { if err != nil {
return return
@@ -230,8 +298,49 @@ func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) {
} }
type NiriParseResult struct { type NiriParseResult struct {
Section *NiriSection Section *NiriSection
DMSBindsIncluded bool DMSBindsIncluded bool
DMSStatus *DMSBindsStatusInfo
ConflictingConfigs map[string]*NiriKeyBinding
}
type DMSBindsStatusInfo struct {
Exists bool
Included bool
IncludePosition int
TotalIncludes int
BindsAfterDMS int
Effective bool
OverriddenBy int
StatusMessage string
}
func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo {
status := &DMSBindsStatusInfo{
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.kdl does not exist"
case !p.dmsBindsIncluded:
status.Effective = false
status.StatusMessage = "dms/binds.kdl is not included in config.kdl"
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 ParseNiriKeys(configDir string) (*NiriParseResult, error) { func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
@@ -241,7 +350,9 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
return nil, err return nil, err
} }
return &NiriParseResult{ return &NiriParseResult{
Section: section, Section: section,
DMSBindsIncluded: parser.HasDMSBindsIncluded(), DMSBindsIncluded: parser.HasDMSBindsIncluded(),
DMSStatus: parser.buildDMSStatus(),
ConflictingConfigs: parser.conflictingConfigs,
}, nil }, nil
} }

View File

@@ -1,11 +1,23 @@
package keybinds package keybinds
type Keybind struct { type Keybind struct {
Key string `json:"key"` Key string `json:"key"`
Description string `json:"desc"` Description string `json:"desc"`
Action string `json:"action,omitempty"` Action string `json:"action,omitempty"`
Subcategory string `json:"subcat,omitempty"` Subcategory string `json:"subcat,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Conflict *Keybind `json:"conflict,omitempty"`
}
type DMSBindsStatus struct {
Exists bool `json:"exists"`
Included bool `json:"included"`
IncludePosition int `json:"includePosition"`
TotalIncludes int `json:"totalIncludes"`
BindsAfterDMS int `json:"bindsAfterDms"`
Effective bool `json:"effective"`
OverriddenBy int `json:"overriddenBy"`
StatusMessage string `json:"statusMessage"`
} }
type CheatSheet struct { type CheatSheet struct {
@@ -13,6 +25,7 @@ type CheatSheet struct {
Provider string `json:"provider"` Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"` Binds map[string][]Keybind `json:"binds"`
DMSBindsIncluded bool `json:"dmsBindsIncluded"` DMSBindsIncluded bool `json:"dmsBindsIncluded"`
DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"`
} }
type Provider interface { type Provider interface {

View File

@@ -18,6 +18,9 @@ Item {
property int _lastDataVersion: -1 property int _lastDataVersion: -1
property var _cachedCategories: [] property var _cachedCategories: []
property var _filteredBinds: [] property var _filteredBinds: []
property real _savedScrollY: 0
property bool _preserveScroll: false
property string _editingKey: ""
function _updateFiltered() { function _updateFiltered() {
const allBinds = KeybindsService.getFlatBinds(); const allBinds = KeybindsService.getFlatBinds();
@@ -87,6 +90,9 @@ Item {
function saveNewBind(bindData) { function saveNewBind(bindData) {
KeybindsService.saveBind("", bindData); KeybindsService.saveBind("", bindData);
showingNewBind = false; showingNewBind = false;
selectedCategory = "";
_editingKey = bindData.key;
expandedKey = bindData.action;
} }
function scrollToTop() { function scrollToTop() {
@@ -102,9 +108,24 @@ Item {
Connections { Connections {
target: KeybindsService target: KeybindsService
function onBindsLoaded() { function onBindsLoaded() {
const savedY = keybindsTab._savedScrollY;
const wasPreserving = keybindsTab._preserveScroll;
keybindsTab._lastDataVersion = KeybindsService._dataVersion; keybindsTab._lastDataVersion = KeybindsService._dataVersion;
keybindsTab._updateCategories(); keybindsTab._updateCategories();
keybindsTab._updateFiltered(); keybindsTab._updateFiltered();
keybindsTab._preserveScroll = false;
if (wasPreserving)
Qt.callLater(() => flickable.contentY = savedY);
}
function onBindSaved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
keybindsTab._editingKey = key;
}
function onBindRemoved(key) {
keybindsTab._savedScrollY = flickable.contentY;
keybindsTab._preserveScroll = true;
keybindsTab._editingKey = "";
} }
} }
@@ -228,14 +249,33 @@ Item {
} }
StyledRect { StyledRect {
id: warningBox
width: Math.min(650, parent.width - Theme.spacingL * 2) width: Math.min(650, parent.width - Theme.spacingL * 2)
height: warningSection.implicitHeight + Theme.spacingL * 2 height: warningSection.implicitHeight + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.error, 0.15)
border.color: Theme.withAlpha(Theme.error, 0.3) readonly property var status: KeybindsService.dmsStatus
readonly property bool showError: !status.included && status.exists
readonly property bool showWarning: status.included && status.overriddenBy > 0
readonly property bool showSetup: !status.exists
color: {
if (showError || showSetup)
return Theme.withAlpha(Theme.error, 0.15);
if (showWarning)
return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15);
return "transparent";
}
border.color: {
if (showError || showSetup)
return Theme.withAlpha(Theme.error, 0.3);
if (showWarning)
return Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3);
return "transparent";
}
border.width: 1 border.width: 1
visible: !KeybindsService.dmsBindsIncluded && !KeybindsService.loading visible: (showError || showWarning || showSetup) && !KeybindsService.loading
Column { Column {
id: warningSection id: warningSection
@@ -248,26 +288,44 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: "warning" name: warningBox.showWarning ? "info" : "warning"
size: Theme.iconSize size: Theme.iconSize
color: Theme.error color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Column { Column {
width: parent.width - Theme.iconSize - 100 - Theme.spacingM * 2 width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: I18n.tr("Binds Include Missing") text: {
if (warningBox.showSetup)
return I18n.tr("First Time Setup");
if (warningBox.showError)
return I18n.tr("Binds Include Missing");
if (warningBox.showWarning)
return I18n.tr("Possible Override Conflicts");
return "";
}
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.error color: warningBox.showWarning ? (Theme.warning ?? Theme.tertiary) : Theme.error
} }
StyledText { StyledText {
text: I18n.tr("dms/binds.kdl is not included in config.kdl. Custom keybinds will not work until this is fixed.") text: {
if (warningBox.showSetup)
return I18n.tr("Click 'Setup' to create dms/binds.kdl and add include to config.kdl.");
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.");
if (warningBox.showWarning) {
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 "";
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -280,12 +338,19 @@ Item {
width: fixButtonText.implicitWidth + Theme.spacingL * 2 width: fixButtonText.implicitWidth + Theme.spacingL * 2
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
visible: warningBox.showError || warningBox.showSetup
color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error color: KeybindsService.fixing ? Theme.withAlpha(Theme.error, 0.6) : Theme.error
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
id: fixButtonText id: fixButtonText
text: KeybindsService.fixing ? I18n.tr("Fixing...") : I18n.tr("Fix Now") text: {
if (KeybindsService.fixing)
return I18n.tr("Fixing...");
if (warningBox.showSetup)
return I18n.tr("Setup");
return I18n.tr("Fix Now");
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surface color: Theme.surface
@@ -532,6 +597,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
bindData: modelData bindData: modelData
isExpanded: keybindsTab.expandedKey === modelData.action isExpanded: keybindsTab.expandedKey === modelData.action
restoreKey: isExpanded ? keybindsTab._editingKey : ""
panelWindow: keybindsTab.parentModal panelWindow: keybindsTab.parentModal
onToggleExpand: keybindsTab.toggleExpanded(modelData.action) onToggleExpand: keybindsTab.toggleExpanded(modelData.action)
onSaveBind: (originalKey, newData) => { onSaveBind: (originalKey, newData) => {
@@ -539,6 +605,7 @@ Item {
keybindsTab.expandedKey = modelData.action; keybindsTab.expandedKey = modelData.action;
} }
onRemoveBind: key => KeybindsService.removeBind(key) onRemoveBind: key => KeybindsService.removeBind(key)
onRestoreKeyConsumed: keybindsTab._editingKey = ""
} }
} }
} }

View File

@@ -28,6 +28,17 @@ Singleton {
property string lastError: "" property string lastError: ""
property bool dmsBindsIncluded: true property bool dmsBindsIncluded: true
property var dmsStatus: ({
exists: true,
included: true,
includePosition: -1,
totalIncludes: 0,
bindsAfterDms: 0,
effective: true,
overriddenBy: 0,
statusMessage: ""
})
property var _rawData: null property var _rawData: null
property var keybinds: ({}) property var keybinds: ({})
property var _allBinds: ({}) property var _allBinds: ({})
@@ -60,6 +71,14 @@ Singleton {
} }
} }
Connections {
target: NiriService
enabled: CompositorService.isNiri
function onConfigReloaded() {
Qt.callLater(root.loadBinds, false);
}
}
Process { Process {
id: loadProcess id: loadProcess
running: false running: false
@@ -178,7 +197,7 @@ Singleton {
} }
function loadBinds(showLoading) { function loadBinds(showLoading) {
if (loading || !available) if (loadProcess.running || !available)
return; return;
const hasData = Object.keys(_allBinds).length > 0; const hasData = Object.keys(_allBinds).length > 0;
loading = showLoading !== false && !hasData; loading = showLoading !== false && !hasData;
@@ -188,8 +207,22 @@ Singleton {
function _processData() { function _processData() {
keybinds = _rawData || {}; keybinds = _rawData || {};
if (currentProvider === "niri") if (currentProvider === "niri") {
dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true; dmsBindsIncluded = _rawData?.dmsBindsIncluded ?? true;
const status = _rawData?.dmsStatus;
if (status) {
dmsStatus = {
exists: status.exists ?? true,
included: status.included ?? true,
includePosition: status.includePosition ?? -1,
totalIncludes: status.totalIncludes ?? 0,
bindsAfterDms: status.bindsAfterDms ?? 0,
effective: status.effective ?? true,
overriddenBy: status.overriddenBy ?? 0,
statusMessage: status.statusMessage ?? ""
};
}
}
if (!_rawData?.binds) { if (!_rawData?.binds) {
_allBinds = {}; _allBinds = {};
@@ -239,12 +272,15 @@ Singleton {
actionMap[action].keys.push(keyData); actionMap[action].keys.push(keyData);
if (!actionMap[action].desc && bind.desc) if (!actionMap[action].desc && bind.desc)
actionMap[action].desc = bind.desc; actionMap[action].desc = bind.desc;
if (!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
}; };
actionMap[action] = entry; actionMap[action] = entry;
grouped.push(entry); grouped.push(entry);

View File

@@ -39,6 +39,7 @@ Singleton {
property string pendingScreenshotPath: "" property string pendingScreenshotPath: ""
signal windowUrgentChanged signal windowUrgentChanged
signal configReloaded
function setWorkspaces(newMap) { function setWorkspaces(newMap) {
root.workspaces = newMap; root.workspaces = newMap;
@@ -508,16 +509,19 @@ Singleton {
function handleConfigLoaded(data) { function handleConfigLoaded(data) {
if (data.failed) { if (data.failed) {
validateProcess.running = true; validateProcess.running = true;
} else { return;
configValidationOutput = ""; }
ToastService.dismissCategory("niri-config");
fetchOutputs(); configValidationOutput = "";
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) { ToastService.dismissCategory("niri-config");
ToastService.showInfo("niri: config reloaded", "", "", "niri-config"); fetchOutputs();
} else if (suppressNextConfigToast) { configReloaded();
suppressNextConfigToast = false;
suppressResetTimer.stop(); if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
} ToastService.showInfo("niri: config reloaded", "", "", "niri-config");
} else if (suppressNextConfigToast) {
suppressNextConfigToast = false;
suppressResetTimer.stop();
} }
if (!hasInitialConnection) { if (!hasInitialConnection) {

View File

@@ -17,6 +17,7 @@ Item {
property var panelWindow: null property var panelWindow: null
property bool recording: false property bool recording: false
property bool isNew: false property bool isNew: false
property string restoreKey: ""
property int editingKeyIndex: -1 property int editingKeyIndex: -1
property string editKey: "" property string editKey: ""
@@ -44,6 +45,8 @@ Item {
} }
return false; return false;
} }
readonly property var configConflict: bindData.conflict || null
readonly property bool hasConfigConflict: configConflict !== null
readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : "" readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : ""
readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : [] readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
readonly property bool hasConflict: _conflicts.length > 0 readonly property bool hasConflict: _conflicts.length > 0
@@ -52,6 +55,7 @@ Item {
signal saveBind(string originalKey, var newData) signal saveBind(string originalKey, var newData)
signal removeBind(string key) signal removeBind(string key)
signal cancelEdit signal cancelEdit
signal restoreKeyConsumed
implicitHeight: contentColumn.implicitHeight implicitHeight: contentColumn.implicitHeight
height: implicitHeight height: implicitHeight
@@ -59,8 +63,37 @@ Item {
Component.onDestruction: _destroyShortcutInhibitor() Component.onDestruction: _destroyShortcutInhibitor()
onIsExpandedChanged: { onIsExpandedChanged: {
if (isExpanded) if (!isExpanded)
return;
if (restoreKey) {
restoreToKey(restoreKey);
restoreKeyConsumed();
} else {
resetEdits(); resetEdits();
}
}
onRestoreKeyChanged: {
if (!isExpanded || !restoreKey)
return;
restoreToKey(restoreKey);
restoreKeyConsumed();
}
function restoreToKey(keyToFind) {
for (let i = 0; i < keys.length; i++) {
if (keys[i].key === keyToFind) {
editingKeyIndex = i;
editKey = keyToFind;
editAction = bindData.action || "";
editDesc = bindData.desc || "";
hasChanges = false;
_actionType = Actions.getActionType(editAction);
useCustomCompositor = _actionType === "compositor" && !Actions.isKnownCompositorAction(editAction);
return;
}
}
resetEdits();
} }
onEditActionChanged: { onEditActionChanged: {
@@ -276,7 +309,21 @@ Item {
text: I18n.tr("Override") text: I18n.tr("Override")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.primary color: Theme.primary
visible: root.hasOverride visible: root.hasOverride && !root.hasConfigConflict
}
DankIcon {
name: "warning"
size: 14
color: Theme.warning ?? Theme.tertiary
visible: root.hasConfigConflict
}
StyledText {
text: I18n.tr("Overridden by config")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning ?? Theme.tertiary
visible: root.hasConfigConflict
} }
Item { Item {
@@ -334,6 +381,58 @@ Item {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingM spacing: Theme.spacingM
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: conflictColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.15)
border.color: Theme.withAlpha(Theme.warning ?? Theme.tertiary, 0.3)
border.width: 1
visible: root.hasConfigConflict
Column {
id: conflictColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: 16
color: Theme.warning ?? Theme.tertiary
}
StyledText {
text: I18n.tr("This bind is overridden by config.kdl")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.warning ?? Theme.tertiary
Layout.fillWidth: true
}
}
StyledText {
text: I18n.tr("Config action: %1").arg(root.configConflict?.action ?? "")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
StyledText {
text: I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.spacingM spacing: Theme.spacingM