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:
@@ -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")
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ type NiriParser struct {
|
|||||||
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 {
|
||||||
@@ -41,20 +51,53 @@ func NewNiriParser(configDir string) *NiriParser {
|
|||||||
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
|
||||||
@@ -232,6 +300,47 @@ 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) {
|
||||||
@@ -243,5 +352,7 @@ func ParseNiriKeys(configDir string) (*NiriParseResult, error) {
|
|||||||
return &NiriParseResult{
|
return &NiriParseResult{
|
||||||
Section: section,
|
Section: section,
|
||||||
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
DMSBindsIncluded: parser.HasDMSBindsIncluded(),
|
||||||
|
DMSStatus: parser.buildDMSStatus(),
|
||||||
|
ConflictingConfigs: parser.conflictingConfigs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ type Keybind struct {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,17 +509,20 @@ Singleton {
|
|||||||
function handleConfigLoaded(data) {
|
function handleConfigLoaded(data) {
|
||||||
if (data.failed) {
|
if (data.failed) {
|
||||||
validateProcess.running = true;
|
validateProcess.running = true;
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
configValidationOutput = "";
|
configValidationOutput = "";
|
||||||
ToastService.dismissCategory("niri-config");
|
ToastService.dismissCategory("niri-config");
|
||||||
fetchOutputs();
|
fetchOutputs();
|
||||||
|
configReloaded();
|
||||||
|
|
||||||
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
|
if (hasInitialConnection && !suppressConfigToast && !suppressNextConfigToast && !matugenSuppression) {
|
||||||
ToastService.showInfo("niri: config reloaded", "", "", "niri-config");
|
ToastService.showInfo("niri: config reloaded", "", "", "niri-config");
|
||||||
} else if (suppressNextConfigToast) {
|
} else if (suppressNextConfigToast) {
|
||||||
suppressNextConfigToast = false;
|
suppressNextConfigToast = false;
|
||||||
suppressResetTimer.stop();
|
suppressResetTimer.stop();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasInitialConnection) {
|
if (!hasInitialConnection) {
|
||||||
hasInitialConnection = true;
|
hasInitialConnection = true;
|
||||||
|
|||||||
@@ -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,7 +63,36 @@ Item {
|
|||||||
Component.onDestruction: _destroyShortcutInhibitor()
|
Component.onDestruction: _destroyShortcutInhibitor()
|
||||||
|
|
||||||
onIsExpandedChanged: {
|
onIsExpandedChanged: {
|
||||||
if (isExpanded)
|
if (!isExpanded)
|
||||||
|
return;
|
||||||
|
if (restoreKey) {
|
||||||
|
restoreToKey(restoreKey);
|
||||||
|
restoreKeyConsumed();
|
||||||
|
} else {
|
||||||
|
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();
|
resetEdits();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user