mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 07:22:50 -05:00
- settings UI for creating, editing, deleting window ruels - IPC to create a window rule for the currently focused toplevel fixes #1292
874 lines
24 KiB
Go
874 lines
24 KiB
Go
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)
|
|
}
|