mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-30 00:12:50 -05:00
niri: add window-rule management
- settings UI for creating, editing, deleting window ruels - IPC to create a window rule for the currently focused toplevel fixes #1292
This commit is contained in:
873
core/internal/windowrules/providers/niri_parser.go
Normal file
873
core/internal/windowrules/providers/niri_parser.go
Normal file
@@ -0,0 +1,873 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sblinch/kdl-go"
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
)
|
||||
|
||||
type NiriWindowRule struct {
|
||||
MatchAppID string
|
||||
MatchTitle string
|
||||
MatchIsFloating *bool
|
||||
MatchIsActive *bool
|
||||
MatchIsFocused *bool
|
||||
MatchIsActiveInColumn *bool
|
||||
MatchIsWindowCastTarget *bool
|
||||
MatchIsUrgent *bool
|
||||
MatchAtStartup *bool
|
||||
Opacity *float64
|
||||
OpenFloating *bool
|
||||
OpenMaximized *bool
|
||||
OpenMaximizedToEdges *bool
|
||||
OpenFullscreen *bool
|
||||
OpenFocused *bool
|
||||
OpenOnOutput string
|
||||
OpenOnWorkspace string
|
||||
DefaultColumnWidth string
|
||||
DefaultWindowHeight string
|
||||
VariableRefreshRate *bool
|
||||
BlockOutFrom string
|
||||
DefaultColumnDisplay string
|
||||
ScrollFactor *float64
|
||||
CornerRadius *int
|
||||
ClipToGeometry *bool
|
||||
TiledState *bool
|
||||
MinWidth *int
|
||||
MaxWidth *int
|
||||
MinHeight *int
|
||||
MaxHeight *int
|
||||
BorderColor string
|
||||
FocusRingColor string
|
||||
FocusRingOff *bool
|
||||
BorderOff *bool
|
||||
DrawBorderWithBg *bool
|
||||
Source string
|
||||
}
|
||||
|
||||
type NiriRulesParser struct {
|
||||
configDir string
|
||||
processedFiles map[string]bool
|
||||
rules []NiriWindowRule
|
||||
currentSource string
|
||||
dmsRulesIncluded bool
|
||||
dmsRulesExists bool
|
||||
includeCount int
|
||||
dmsIncludePos int
|
||||
rulesAfterDMS int
|
||||
dmsProcessed bool
|
||||
}
|
||||
|
||||
func NewNiriRulesParser(configDir string) *NiriRulesParser {
|
||||
return &NiriRulesParser{
|
||||
configDir: configDir,
|
||||
processedFiles: make(map[string]bool),
|
||||
rules: []NiriWindowRule{},
|
||||
dmsIncludePos: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) Parse() ([]NiriWindowRule, error) {
|
||||
dmsRulesPath := filepath.Join(p.configDir, "dms", "windowrules.kdl")
|
||||
if _, err := os.Stat(dmsRulesPath); err == nil {
|
||||
p.dmsRulesExists = true
|
||||
}
|
||||
|
||||
configPath := filepath.Join(p.configDir, "config.kdl")
|
||||
if err := p.parseFile(configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.dmsRulesExists && !p.dmsProcessed {
|
||||
p.parseDMSRulesDirectly(dmsRulesPath)
|
||||
}
|
||||
|
||||
return p.rules, nil
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseDMSRulesDirectly(dmsRulesPath string) {
|
||||
data, err := os.ReadFile(dmsRulesPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = dmsRulesPath
|
||||
p.processNodes(doc.Nodes, filepath.Dir(dmsRulesPath))
|
||||
p.currentSource = prevSource
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseFile(filePath string) error {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.processedFiles[absPath] {
|
||||
return nil
|
||||
}
|
||||
p.processedFiles[absPath] = true
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = absPath
|
||||
baseDir := filepath.Dir(absPath)
|
||||
p.processNodes(doc.Nodes, baseDir)
|
||||
p.currentSource = prevSource
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) processNodes(nodes []*document.Node, baseDir string) {
|
||||
for _, node := range nodes {
|
||||
name := node.Name.String()
|
||||
|
||||
switch name {
|
||||
case "include":
|
||||
p.handleInclude(node, baseDir)
|
||||
case "window-rule":
|
||||
p.parseWindowRuleNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) handleInclude(node *document.Node, baseDir string) {
|
||||
if len(node.Arguments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||
isDMSInclude := includePath == "dms/windowrules.kdl" || strings.HasSuffix(includePath, "/dms/windowrules.kdl")
|
||||
|
||||
p.includeCount++
|
||||
if isDMSInclude {
|
||||
p.dmsRulesIncluded = true
|
||||
p.dmsIncludePos = p.includeCount
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(baseDir, includePath)
|
||||
if filepath.IsAbs(includePath) {
|
||||
fullPath = includePath
|
||||
}
|
||||
|
||||
_ = p.parseFile(fullPath)
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) {
|
||||
if node.Children == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rule := NiriWindowRule{
|
||||
Source: p.currentSource,
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
childName := child.Name.String()
|
||||
|
||||
switch childName {
|
||||
case "match":
|
||||
p.parseMatchNode(child, &rule)
|
||||
case "opacity":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if f, ok := val.(float64); ok {
|
||||
rule.Opacity = &f
|
||||
}
|
||||
}
|
||||
case "open-floating":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.OpenFloating = &b
|
||||
case "open-maximized":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.OpenMaximized = &b
|
||||
case "open-maximized-to-edges":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.OpenMaximizedToEdges = &b
|
||||
case "open-fullscreen":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.OpenFullscreen = &b
|
||||
case "open-focused":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.OpenFocused = &b
|
||||
case "open-on-output":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.OpenOnOutput = child.Arguments[0].ValueString()
|
||||
}
|
||||
case "open-on-workspace":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.OpenOnWorkspace = child.Arguments[0].ValueString()
|
||||
}
|
||||
case "default-column-width":
|
||||
rule.DefaultColumnWidth = p.parseSizeNode(child)
|
||||
case "default-window-height":
|
||||
rule.DefaultWindowHeight = p.parseSizeNode(child)
|
||||
case "variable-refresh-rate":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.VariableRefreshRate = &b
|
||||
case "block-out-from":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.BlockOutFrom = child.Arguments[0].ValueString()
|
||||
}
|
||||
case "default-column-display":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.DefaultColumnDisplay = child.Arguments[0].ValueString()
|
||||
}
|
||||
case "scroll-factor":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if f, ok := val.(float64); ok {
|
||||
rule.ScrollFactor = &f
|
||||
}
|
||||
}
|
||||
case "geometry-corner-radius":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if i, ok := val.(int64); ok {
|
||||
intVal := int(i)
|
||||
rule.CornerRadius = &intVal
|
||||
}
|
||||
}
|
||||
case "clip-to-geometry":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.ClipToGeometry = &b
|
||||
case "tiled-state":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.TiledState = &b
|
||||
case "min-width":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if i, ok := val.(int64); ok {
|
||||
intVal := int(i)
|
||||
rule.MinWidth = &intVal
|
||||
}
|
||||
}
|
||||
case "max-width":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if i, ok := val.(int64); ok {
|
||||
intVal := int(i)
|
||||
rule.MaxWidth = &intVal
|
||||
}
|
||||
}
|
||||
case "min-height":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if i, ok := val.(int64); ok {
|
||||
intVal := int(i)
|
||||
rule.MinHeight = &intVal
|
||||
}
|
||||
}
|
||||
case "max-height":
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
if i, ok := val.(int64); ok {
|
||||
intVal := int(i)
|
||||
rule.MaxHeight = &intVal
|
||||
}
|
||||
}
|
||||
case "border":
|
||||
p.parseBorderNode(child, &rule)
|
||||
case "focus-ring":
|
||||
p.parseFocusRingNode(child, &rule)
|
||||
case "draw-border-with-background":
|
||||
b := p.parseBoolArg(child)
|
||||
rule.DrawBorderWithBg = &b
|
||||
}
|
||||
}
|
||||
|
||||
p.rules = append(p.rules, rule)
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseSizeNode(node *document.Node) string {
|
||||
if node.Children == nil {
|
||||
return ""
|
||||
}
|
||||
for _, child := range node.Children {
|
||||
name := child.Name.String()
|
||||
if len(child.Arguments) > 0 {
|
||||
val := child.Arguments[0].ResolvedValue()
|
||||
switch name {
|
||||
case "fixed":
|
||||
if i, ok := val.(int64); ok {
|
||||
return "fixed " + strconv.FormatInt(i, 10)
|
||||
}
|
||||
case "proportion":
|
||||
if f, ok := val.(float64); ok {
|
||||
return "proportion " + strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseMatchNode(node *document.Node, rule *NiriWindowRule) {
|
||||
if node.Properties == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if val, ok := node.Properties.Get("app-id"); ok {
|
||||
rule.MatchAppID = val.ValueString()
|
||||
}
|
||||
if val, ok := node.Properties.Get("title"); ok {
|
||||
rule.MatchTitle = val.ValueString()
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-floating"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsFloating = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-active"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsActive = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-focused"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsFocused = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-active-in-column"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsActiveInColumn = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-window-cast-target"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsWindowCastTarget = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("is-urgent"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchIsUrgent = &b
|
||||
}
|
||||
if val, ok := node.Properties.Get("at-startup"); ok {
|
||||
b := val.ValueString() == "true"
|
||||
rule.MatchAtStartup = &b
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseBorderNode(node *document.Node, rule *NiriWindowRule) {
|
||||
if node.Children == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
switch child.Name.String() {
|
||||
case "off":
|
||||
b := true
|
||||
rule.BorderOff = &b
|
||||
case "active-color":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.BorderColor = child.Arguments[0].ValueString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseFocusRingNode(node *document.Node, rule *NiriWindowRule) {
|
||||
if node.Children == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, child := range node.Children {
|
||||
switch child.Name.String() {
|
||||
case "off":
|
||||
b := true
|
||||
rule.FocusRingOff = &b
|
||||
case "active-color":
|
||||
if len(child.Arguments) > 0 {
|
||||
rule.FocusRingColor = child.Arguments[0].ValueString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) parseBoolArg(node *document.Node) bool {
|
||||
if len(node.Arguments) == 0 {
|
||||
return true
|
||||
}
|
||||
return node.Arguments[0].ValueString() != "false"
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) HasDMSRulesIncluded() bool {
|
||||
return p.dmsRulesIncluded
|
||||
}
|
||||
|
||||
func (p *NiriRulesParser) buildDMSStatus() *windowrules.DMSRulesStatus {
|
||||
status := &windowrules.DMSRulesStatus{
|
||||
Exists: p.dmsRulesExists,
|
||||
Included: p.dmsRulesIncluded,
|
||||
IncludePosition: p.dmsIncludePos,
|
||||
TotalIncludes: p.includeCount,
|
||||
RulesAfterDMS: p.rulesAfterDMS,
|
||||
}
|
||||
|
||||
switch {
|
||||
case !p.dmsRulesExists:
|
||||
status.Effective = false
|
||||
status.StatusMessage = "dms/windowrules.kdl does not exist"
|
||||
case !p.dmsRulesIncluded:
|
||||
status.Effective = false
|
||||
status.StatusMessage = "dms/windowrules.kdl is not included in config.kdl"
|
||||
case p.rulesAfterDMS > 0:
|
||||
status.Effective = true
|
||||
status.OverriddenBy = p.rulesAfterDMS
|
||||
status.StatusMessage = "Some DMS rules may be overridden by config rules"
|
||||
default:
|
||||
status.Effective = true
|
||||
status.StatusMessage = "DMS window rules are active"
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
type NiriRulesParseResult struct {
|
||||
Rules []NiriWindowRule
|
||||
DMSRulesIncluded bool
|
||||
DMSStatus *windowrules.DMSRulesStatus
|
||||
}
|
||||
|
||||
func ParseNiriWindowRules(configDir string) (*NiriRulesParseResult, error) {
|
||||
parser := NewNiriRulesParser(configDir)
|
||||
rules, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &NiriRulesParseResult{
|
||||
Rules: rules,
|
||||
DMSRulesIncluded: parser.HasDMSRulesIncluded(),
|
||||
DMSStatus: parser.buildDMSStatus(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.WindowRule {
|
||||
result := make([]windowrules.WindowRule, 0, len(niriRules))
|
||||
for i, nr := range niriRules {
|
||||
wr := windowrules.WindowRule{
|
||||
ID: fmt.Sprintf("rule_%d", i),
|
||||
Enabled: true,
|
||||
Source: nr.Source,
|
||||
MatchCriteria: windowrules.MatchCriteria{
|
||||
AppID: nr.MatchAppID,
|
||||
Title: nr.MatchTitle,
|
||||
IsFloating: nr.MatchIsFloating,
|
||||
IsActive: nr.MatchIsActive,
|
||||
IsFocused: nr.MatchIsFocused,
|
||||
IsActiveInColumn: nr.MatchIsActiveInColumn,
|
||||
IsWindowCastTarget: nr.MatchIsWindowCastTarget,
|
||||
IsUrgent: nr.MatchIsUrgent,
|
||||
AtStartup: nr.MatchAtStartup,
|
||||
},
|
||||
Actions: windowrules.Actions{
|
||||
Opacity: nr.Opacity,
|
||||
OpenFloating: nr.OpenFloating,
|
||||
OpenMaximized: nr.OpenMaximized,
|
||||
OpenMaximizedToEdges: nr.OpenMaximizedToEdges,
|
||||
OpenFullscreen: nr.OpenFullscreen,
|
||||
OpenFocused: nr.OpenFocused,
|
||||
OpenOnOutput: nr.OpenOnOutput,
|
||||
OpenOnWorkspace: nr.OpenOnWorkspace,
|
||||
DefaultColumnWidth: nr.DefaultColumnWidth,
|
||||
DefaultWindowHeight: nr.DefaultWindowHeight,
|
||||
VariableRefreshRate: nr.VariableRefreshRate,
|
||||
BlockOutFrom: nr.BlockOutFrom,
|
||||
DefaultColumnDisplay: nr.DefaultColumnDisplay,
|
||||
ScrollFactor: nr.ScrollFactor,
|
||||
CornerRadius: nr.CornerRadius,
|
||||
ClipToGeometry: nr.ClipToGeometry,
|
||||
TiledState: nr.TiledState,
|
||||
MinWidth: nr.MinWidth,
|
||||
MaxWidth: nr.MaxWidth,
|
||||
MinHeight: nr.MinHeight,
|
||||
MaxHeight: nr.MaxHeight,
|
||||
BorderColor: nr.BorderColor,
|
||||
FocusRingColor: nr.FocusRingColor,
|
||||
FocusRingOff: nr.FocusRingOff,
|
||||
BorderOff: nr.BorderOff,
|
||||
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||
},
|
||||
}
|
||||
result = append(result, wr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type NiriWritableProvider struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
func NewNiriWritableProvider(configDir string) *NiriWritableProvider {
|
||||
return &NiriWritableProvider{configDir: configDir}
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) Name() string {
|
||||
return "niri"
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) GetOverridePath() string {
|
||||
return filepath.Join(p.configDir, "dms", "windowrules.kdl")
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
|
||||
result, err := ParseNiriWindowRules(p.configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &windowrules.RuleSet{
|
||||
Title: "Niri Window Rules",
|
||||
Provider: "niri",
|
||||
Rules: ConvertNiriRulesToWindowRules(result.Rules),
|
||||
DMSRulesIncluded: result.DMSRulesIncluded,
|
||||
DMSStatus: result.DMSStatus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) SetRule(rule windowrules.WindowRule) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
rules = []windowrules.WindowRule{}
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, r := range rules {
|
||||
if r.ID == rule.ID {
|
||||
rules[i] = rule
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return p.writeDMSRules(rules)
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) RemoveRule(id string) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newRules := make([]windowrules.WindowRule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
if r.ID != id {
|
||||
newRules = append(newRules, r)
|
||||
}
|
||||
}
|
||||
|
||||
return p.writeDMSRules(newRules)
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) ReorderRules(ids []string) error {
|
||||
rules, err := p.LoadDMSRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ruleMap := make(map[string]windowrules.WindowRule)
|
||||
for _, r := range rules {
|
||||
ruleMap[r.ID] = r
|
||||
}
|
||||
|
||||
newRules := make([]windowrules.WindowRule, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if r, ok := ruleMap[id]; ok {
|
||||
newRules = append(newRules, r)
|
||||
delete(ruleMap, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range ruleMap {
|
||||
newRules = append(newRules, r)
|
||||
}
|
||||
|
||||
return p.writeDMSRules(newRules)
|
||||
}
|
||||
|
||||
var niriMetaCommentRegex = regexp.MustCompile(`^//\s*@id=(\S*)\s*@name=(.*)$`)
|
||||
|
||||
func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) {
|
||||
rulesPath := p.GetOverridePath()
|
||||
data, err := os.ReadFile(rulesPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []windowrules.WindowRule{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
type ruleMeta struct {
|
||||
id string
|
||||
name string
|
||||
}
|
||||
var metas []ruleMeta
|
||||
var currentID, currentName string
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if matches := niriMetaCommentRegex.FindStringSubmatch(trimmed); matches != nil {
|
||||
currentID = matches[1]
|
||||
currentName = strings.TrimSpace(matches[2])
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "window-rule") {
|
||||
metas = append(metas, ruleMeta{id: currentID, name: currentName})
|
||||
currentID = ""
|
||||
currentName = ""
|
||||
}
|
||||
}
|
||||
|
||||
doc, err := kdl.Parse(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parser := NewNiriRulesParser(p.configDir)
|
||||
parser.currentSource = rulesPath
|
||||
|
||||
for _, node := range doc.Nodes {
|
||||
if node.Name.String() == "window-rule" {
|
||||
parser.parseWindowRuleNode(node)
|
||||
}
|
||||
}
|
||||
|
||||
var rules []windowrules.WindowRule
|
||||
for i, nr := range parser.rules {
|
||||
id := ""
|
||||
name := ""
|
||||
if i < len(metas) {
|
||||
id = metas[i].id
|
||||
name = metas[i].name
|
||||
}
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("dms_rule_%d", i)
|
||||
}
|
||||
|
||||
wr := windowrules.WindowRule{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Enabled: true,
|
||||
Source: rulesPath,
|
||||
MatchCriteria: windowrules.MatchCriteria{
|
||||
AppID: nr.MatchAppID,
|
||||
Title: nr.MatchTitle,
|
||||
IsFloating: nr.MatchIsFloating,
|
||||
IsActive: nr.MatchIsActive,
|
||||
IsFocused: nr.MatchIsFocused,
|
||||
IsActiveInColumn: nr.MatchIsActiveInColumn,
|
||||
IsWindowCastTarget: nr.MatchIsWindowCastTarget,
|
||||
IsUrgent: nr.MatchIsUrgent,
|
||||
AtStartup: nr.MatchAtStartup,
|
||||
},
|
||||
Actions: windowrules.Actions{
|
||||
Opacity: nr.Opacity,
|
||||
OpenFloating: nr.OpenFloating,
|
||||
OpenMaximized: nr.OpenMaximized,
|
||||
OpenMaximizedToEdges: nr.OpenMaximizedToEdges,
|
||||
OpenFullscreen: nr.OpenFullscreen,
|
||||
OpenFocused: nr.OpenFocused,
|
||||
OpenOnOutput: nr.OpenOnOutput,
|
||||
OpenOnWorkspace: nr.OpenOnWorkspace,
|
||||
DefaultColumnWidth: nr.DefaultColumnWidth,
|
||||
DefaultWindowHeight: nr.DefaultWindowHeight,
|
||||
VariableRefreshRate: nr.VariableRefreshRate,
|
||||
BlockOutFrom: nr.BlockOutFrom,
|
||||
DefaultColumnDisplay: nr.DefaultColumnDisplay,
|
||||
ScrollFactor: nr.ScrollFactor,
|
||||
CornerRadius: nr.CornerRadius,
|
||||
ClipToGeometry: nr.ClipToGeometry,
|
||||
TiledState: nr.TiledState,
|
||||
MinWidth: nr.MinWidth,
|
||||
MaxWidth: nr.MaxWidth,
|
||||
MinHeight: nr.MinHeight,
|
||||
MaxHeight: nr.MaxHeight,
|
||||
BorderColor: nr.BorderColor,
|
||||
FocusRingColor: nr.FocusRingColor,
|
||||
FocusRingOff: nr.FocusRingOff,
|
||||
BorderOff: nr.BorderOff,
|
||||
DrawBorderWithBg: nr.DrawBorderWithBg,
|
||||
},
|
||||
}
|
||||
|
||||
rules = append(rules, wr)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) writeDMSRules(rules []windowrules.WindowRule) error {
|
||||
rulesPath := p.GetOverridePath()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(rulesPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lines []string
|
||||
lines = append(lines, "// DMS Window Rules - Managed by DankMaterialShell")
|
||||
lines = append(lines, "// Do not edit manually - changes may be overwritten")
|
||||
lines = append(lines, "")
|
||||
|
||||
for _, rule := range rules {
|
||||
lines = append(lines, p.formatRule(rule))
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644)
|
||||
}
|
||||
|
||||
func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("// @id=%s @name=%s", rule.ID, rule.Name))
|
||||
lines = append(lines, "window-rule {")
|
||||
|
||||
m := rule.MatchCriteria
|
||||
if m.AppID != "" || m.Title != "" || m.IsFloating != nil || m.IsActive != nil ||
|
||||
m.IsFocused != nil || m.IsActiveInColumn != nil || m.IsWindowCastTarget != nil ||
|
||||
m.IsUrgent != nil || m.AtStartup != nil {
|
||||
var matchProps []string
|
||||
if m.AppID != "" {
|
||||
matchProps = append(matchProps, fmt.Sprintf("app-id=%q", m.AppID))
|
||||
}
|
||||
if m.Title != "" {
|
||||
matchProps = append(matchProps, fmt.Sprintf("title=%q", m.Title))
|
||||
}
|
||||
if m.IsFloating != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-floating=%t", *m.IsFloating))
|
||||
}
|
||||
if m.IsActive != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-active=%t", *m.IsActive))
|
||||
}
|
||||
if m.IsFocused != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-focused=%t", *m.IsFocused))
|
||||
}
|
||||
if m.IsActiveInColumn != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-active-in-column=%t", *m.IsActiveInColumn))
|
||||
}
|
||||
if m.IsWindowCastTarget != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-window-cast-target=%t", *m.IsWindowCastTarget))
|
||||
}
|
||||
if m.IsUrgent != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("is-urgent=%t", *m.IsUrgent))
|
||||
}
|
||||
if m.AtStartup != nil {
|
||||
matchProps = append(matchProps, fmt.Sprintf("at-startup=%t", *m.AtStartup))
|
||||
}
|
||||
lines = append(lines, " match "+strings.Join(matchProps, " "))
|
||||
}
|
||||
|
||||
a := rule.Actions
|
||||
if a.Opacity != nil {
|
||||
lines = append(lines, fmt.Sprintf(" opacity %.2f", *a.Opacity))
|
||||
}
|
||||
if a.OpenFloating != nil && *a.OpenFloating {
|
||||
lines = append(lines, " open-floating true")
|
||||
}
|
||||
if a.OpenMaximized != nil && *a.OpenMaximized {
|
||||
lines = append(lines, " open-maximized true")
|
||||
}
|
||||
if a.OpenMaximizedToEdges != nil && *a.OpenMaximizedToEdges {
|
||||
lines = append(lines, " open-maximized-to-edges true")
|
||||
}
|
||||
if a.OpenFullscreen != nil && *a.OpenFullscreen {
|
||||
lines = append(lines, " open-fullscreen true")
|
||||
}
|
||||
if a.OpenFocused != nil {
|
||||
lines = append(lines, fmt.Sprintf(" open-focused %t", *a.OpenFocused))
|
||||
}
|
||||
if a.OpenOnOutput != "" {
|
||||
lines = append(lines, fmt.Sprintf(" open-on-output %q", a.OpenOnOutput))
|
||||
}
|
||||
if a.OpenOnWorkspace != "" {
|
||||
lines = append(lines, fmt.Sprintf(" open-on-workspace %q", a.OpenOnWorkspace))
|
||||
}
|
||||
if a.DefaultColumnWidth != "" {
|
||||
lines = append(lines, formatSizeProperty("default-column-width", a.DefaultColumnWidth))
|
||||
}
|
||||
if a.DefaultWindowHeight != "" {
|
||||
lines = append(lines, formatSizeProperty("default-window-height", a.DefaultWindowHeight))
|
||||
}
|
||||
if a.VariableRefreshRate != nil && *a.VariableRefreshRate {
|
||||
lines = append(lines, " variable-refresh-rate true")
|
||||
}
|
||||
if a.BlockOutFrom != "" {
|
||||
lines = append(lines, fmt.Sprintf(" block-out-from %q", a.BlockOutFrom))
|
||||
}
|
||||
if a.DefaultColumnDisplay != "" {
|
||||
lines = append(lines, fmt.Sprintf(" default-column-display %q", a.DefaultColumnDisplay))
|
||||
}
|
||||
if a.ScrollFactor != nil {
|
||||
lines = append(lines, fmt.Sprintf(" scroll-factor %.2f", *a.ScrollFactor))
|
||||
}
|
||||
if a.CornerRadius != nil {
|
||||
lines = append(lines, fmt.Sprintf(" geometry-corner-radius %d", *a.CornerRadius))
|
||||
}
|
||||
if a.ClipToGeometry != nil && *a.ClipToGeometry {
|
||||
lines = append(lines, " clip-to-geometry true")
|
||||
}
|
||||
if a.TiledState != nil && *a.TiledState {
|
||||
lines = append(lines, " tiled-state true")
|
||||
}
|
||||
if a.MinWidth != nil {
|
||||
lines = append(lines, fmt.Sprintf(" min-width %d", *a.MinWidth))
|
||||
}
|
||||
if a.MaxWidth != nil {
|
||||
lines = append(lines, fmt.Sprintf(" max-width %d", *a.MaxWidth))
|
||||
}
|
||||
if a.MinHeight != nil {
|
||||
lines = append(lines, fmt.Sprintf(" min-height %d", *a.MinHeight))
|
||||
}
|
||||
if a.MaxHeight != nil {
|
||||
lines = append(lines, fmt.Sprintf(" max-height %d", *a.MaxHeight))
|
||||
}
|
||||
if a.BorderOff != nil && *a.BorderOff {
|
||||
lines = append(lines, " border { off; }")
|
||||
} else if a.BorderColor != "" {
|
||||
lines = append(lines, fmt.Sprintf(" border { active-color %q; }", a.BorderColor))
|
||||
}
|
||||
if a.FocusRingOff != nil && *a.FocusRingOff {
|
||||
lines = append(lines, " focus-ring { off; }")
|
||||
} else if a.FocusRingColor != "" {
|
||||
lines = append(lines, fmt.Sprintf(" focus-ring { active-color %q; }", a.FocusRingColor))
|
||||
}
|
||||
if a.DrawBorderWithBg != nil {
|
||||
lines = append(lines, fmt.Sprintf(" draw-border-with-background %t", *a.DrawBorderWithBg))
|
||||
}
|
||||
|
||||
lines = append(lines, "}")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func formatSizeProperty(name, value string) string {
|
||||
parts := strings.SplitN(value, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Sprintf(" %s { }", name)
|
||||
}
|
||||
sizeType := parts[0]
|
||||
sizeValue := parts[1]
|
||||
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
|
||||
}
|
||||
Reference in New Issue
Block a user