From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../windowrules/providers/hyprland_parser.go | 658 ---------------- .../windowrules/providers/hyprland_parser_test.go | 280 ------- .../internal/windowrules/providers/niri_parser.go | 875 --------------------- .../windowrules/providers/niri_parser_test.go | 335 -------- .../windowrules/providers/providers_test.go | 22 - .../core/internal/windowrules/types.go | 103 --- 6 files changed, 2273 deletions(-) delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser.go delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser_test.go delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser.go delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser_test.go delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/providers_test.go delete mode 100644 raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/types.go (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules') diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser.go deleted file mode 100644 index 918c4f9..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser.go +++ /dev/null @@ -1,658 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules" -) - -type HyprlandWindowRule struct { - MatchClass string - MatchTitle string - MatchXWayland *bool - MatchFloating *bool - MatchFullscreen *bool - MatchPinned *bool - MatchInitialised *bool - Rule string - Value string - Source string - RawLine string -} - -type HyprlandRulesParser struct { - configDir string - processedFiles map[string]bool - rules []HyprlandWindowRule - currentSource string - dmsRulesExists bool - dmsRulesIncluded bool - includeCount int - dmsIncludePos int - rulesAfterDMS int - dmsProcessed bool -} - -func NewHyprlandRulesParser(configDir string) *HyprlandRulesParser { - return &HyprlandRulesParser{ - configDir: configDir, - processedFiles: make(map[string]bool), - rules: []HyprlandWindowRule{}, - dmsIncludePos: -1, - } -} - -func (p *HyprlandRulesParser) Parse() ([]HyprlandWindowRule, error) { - expandedDir, err := utils.ExpandPath(p.configDir) - if err != nil { - return nil, err - } - - dmsRulesPath := filepath.Join(expandedDir, "dms", "windowrules.conf") - if _, err := os.Stat(dmsRulesPath); err == nil { - p.dmsRulesExists = true - } - - mainConfig := filepath.Join(expandedDir, "hyprland.conf") - if err := p.parseFile(mainConfig); err != nil { - return nil, err - } - - if p.dmsRulesExists && !p.dmsProcessed { - p.parseDMSRulesDirectly(dmsRulesPath) - } - - return p.rules, nil -} - -func (p *HyprlandRulesParser) parseDMSRulesDirectly(dmsRulesPath string) { - data, err := os.ReadFile(dmsRulesPath) - if err != nil { - return - } - - prevSource := p.currentSource - p.currentSource = dmsRulesPath - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - p.parseLine(line) - } - - p.currentSource = prevSource - p.dmsProcessed = true -} - -func (p *HyprlandRulesParser) 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 - } - - prevSource := p.currentSource - p.currentSource = absPath - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - - if strings.HasPrefix(trimmed, "source") { - p.handleSource(trimmed, filepath.Dir(absPath)) - continue - } - - p.parseLine(line) - } - - p.currentSource = prevSource - return nil -} - -func (p *HyprlandRulesParser) handleSource(line string, baseDir string) { - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - return - } - - sourcePath := strings.TrimSpace(parts[1]) - isDMSSource := sourcePath == "dms/windowrules.conf" || strings.HasSuffix(sourcePath, "/dms/windowrules.conf") - - p.includeCount++ - if isDMSSource { - p.dmsRulesIncluded = true - p.dmsIncludePos = p.includeCount - p.dmsProcessed = true - } - - fullPath := sourcePath - if !filepath.IsAbs(sourcePath) { - fullPath = filepath.Join(baseDir, sourcePath) - } - - expanded, err := utils.ExpandPath(fullPath) - if err != nil { - return - } - - _ = p.parseFile(expanded) -} - -func (p *HyprlandRulesParser) parseLine(line string) { - trimmed := strings.TrimSpace(line) - - if strings.HasPrefix(trimmed, "windowrule") { - rule := p.parseWindowRuleLine(trimmed) - if rule != nil { - rule.Source = p.currentSource - p.rules = append(p.rules, *rule) - } - } -} - -var windowRuleV2Regex = regexp.MustCompile(`^windowrulev?2?\s*=\s*(.+)$`) - -func (p *HyprlandRulesParser) parseWindowRuleLine(line string) *HyprlandWindowRule { - matches := windowRuleV2Regex.FindStringSubmatch(line) - if len(matches) < 2 { - return nil - } - - content := strings.TrimSpace(matches[1]) - isV2 := strings.HasPrefix(line, "windowrulev2") - - rule := &HyprlandWindowRule{ - RawLine: line, - } - - if isV2 { - p.parseWindowRuleV2(content, rule) - } else { - p.parseWindowRuleV1(content, rule) - } - - return rule -} - -func (p *HyprlandRulesParser) parseWindowRuleV1(content string, rule *HyprlandWindowRule) { - parts := strings.SplitN(content, ",", 2) - if len(parts) < 2 { - return - } - - rule.Rule = strings.TrimSpace(parts[0]) - rule.MatchClass = strings.TrimSpace(parts[1]) -} - -func (p *HyprlandRulesParser) parseWindowRuleV2(content string, rule *HyprlandWindowRule) { - parts := strings.SplitN(content, ",", 2) - if len(parts) < 2 { - return - } - - ruleAndValue := strings.TrimSpace(parts[0]) - matchPart := strings.TrimSpace(parts[1]) - - if idx := strings.Index(ruleAndValue, " "); idx > 0 { - rule.Rule = ruleAndValue[:idx] - rule.Value = strings.TrimSpace(ruleAndValue[idx+1:]) - } else { - rule.Rule = ruleAndValue - } - - matchPairs := strings.Split(matchPart, ",") - for _, pair := range matchPairs { - pair = strings.TrimSpace(pair) - if colonIdx := strings.Index(pair, ":"); colonIdx > 0 { - key := strings.TrimSpace(pair[:colonIdx]) - value := strings.TrimSpace(pair[colonIdx+1:]) - - switch key { - case "class": - rule.MatchClass = value - case "title": - rule.MatchTitle = value - case "xwayland": - b := value == "1" || value == "true" - rule.MatchXWayland = &b - case "floating": - b := value == "1" || value == "true" - rule.MatchFloating = &b - case "fullscreen": - b := value == "1" || value == "true" - rule.MatchFullscreen = &b - case "pinned": - b := value == "1" || value == "true" - rule.MatchPinned = &b - case "initialised", "initialized": - b := value == "1" || value == "true" - rule.MatchInitialised = &b - } - } - } -} - -func (p *HyprlandRulesParser) HasDMSRulesIncluded() bool { - return p.dmsRulesIncluded -} - -func (p *HyprlandRulesParser) 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.conf does not exist" - case !p.dmsRulesIncluded: - status.Effective = false - status.StatusMessage = "dms/windowrules.conf is not sourced in config" - 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 HyprlandRulesParseResult struct { - Rules []HyprlandWindowRule - DMSRulesIncluded bool - DMSStatus *windowrules.DMSRulesStatus -} - -func ParseHyprlandWindowRules(configDir string) (*HyprlandRulesParseResult, error) { - parser := NewHyprlandRulesParser(configDir) - rules, err := parser.Parse() - if err != nil { - return nil, err - } - return &HyprlandRulesParseResult{ - Rules: rules, - DMSRulesIncluded: parser.HasDMSRulesIncluded(), - DMSStatus: parser.buildDMSStatus(), - }, nil -} - -func applyHyprlandRuleAction(actions *windowrules.Actions, rule, value string) { - t := true - switch rule { - case "float": - actions.OpenFloating = &t - case "tile": - actions.Tile = &t - case "fullscreen": - actions.OpenFullscreen = &t - case "maximize": - actions.OpenMaximized = &t - case "nofocus": - actions.NoFocus = &t - case "noborder": - actions.NoBorder = &t - case "noshadow": - actions.NoShadow = &t - case "nodim": - actions.NoDim = &t - case "noblur": - actions.NoBlur = &t - case "noanim": - actions.NoAnim = &t - case "norounding": - actions.NoRounding = &t - case "pin": - actions.Pin = &t - case "opaque": - actions.Opaque = &t - case "forcergbx": - actions.ForcergbX = &t - case "opacity": - if f, err := strconv.ParseFloat(value, 64); err == nil { - actions.Opacity = &f - } - case "size": - actions.Size = value - case "move": - actions.Move = value - case "monitor": - actions.Monitor = value - case "workspace": - actions.Workspace = value - case "idleinhibit": - actions.Idleinhibit = value - case "rounding": - if i, err := strconv.Atoi(value); err == nil { - actions.CornerRadius = &i - } - } -} - -func ConvertHyprlandRulesToWindowRules(hyprRules []HyprlandWindowRule) []windowrules.WindowRule { - result := make([]windowrules.WindowRule, 0, len(hyprRules)) - for i, hr := range hyprRules { - wr := windowrules.WindowRule{ - ID: strconv.Itoa(i), - Enabled: true, - Source: hr.Source, - MatchCriteria: windowrules.MatchCriteria{ - AppID: hr.MatchClass, - Title: hr.MatchTitle, - XWayland: hr.MatchXWayland, - IsFloating: hr.MatchFloating, - Fullscreen: hr.MatchFullscreen, - Pinned: hr.MatchPinned, - Initialised: hr.MatchInitialised, - }, - } - applyHyprlandRuleAction(&wr.Actions, hr.Rule, hr.Value) - result = append(result, wr) - } - return result -} - -type HyprlandWritableProvider struct { - configDir string -} - -func NewHyprlandWritableProvider(configDir string) *HyprlandWritableProvider { - return &HyprlandWritableProvider{configDir: configDir} -} - -func (p *HyprlandWritableProvider) Name() string { - return "hyprland" -} - -func (p *HyprlandWritableProvider) GetOverridePath() string { - expanded, _ := utils.ExpandPath(p.configDir) - return filepath.Join(expanded, "dms", "windowrules.conf") -} - -func (p *HyprlandWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) { - result, err := ParseHyprlandWindowRules(p.configDir) - if err != nil { - return nil, err - } - return &windowrules.RuleSet{ - Title: "Hyprland Window Rules", - Provider: "hyprland", - Rules: ConvertHyprlandRulesToWindowRules(result.Rules), - DMSRulesIncluded: result.DMSRulesIncluded, - DMSStatus: result.DMSStatus, - }, nil -} - -func (p *HyprlandWritableProvider) 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 *HyprlandWritableProvider) 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 *HyprlandWritableProvider) 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 dmsRuleCommentRegex = regexp.MustCompile(`^#\s*DMS-RULE:\s*id=([^,]+),\s*name=(.*)$`) - -func (p *HyprlandWritableProvider) 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 - } - - var rules []windowrules.WindowRule - var currentID, currentName string - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - - if matches := dmsRuleCommentRegex.FindStringSubmatch(trimmed); matches != nil { - currentID = matches[1] - currentName = matches[2] - continue - } - - if strings.HasPrefix(trimmed, "windowrulev2") { - parser := NewHyprlandRulesParser(p.configDir) - hrule := parser.parseWindowRuleLine(trimmed) - if hrule == nil { - continue - } - - wr := windowrules.WindowRule{ - ID: currentID, - Name: currentName, - Enabled: true, - Source: rulesPath, - MatchCriteria: windowrules.MatchCriteria{ - AppID: hrule.MatchClass, - Title: hrule.MatchTitle, - XWayland: hrule.MatchXWayland, - IsFloating: hrule.MatchFloating, - Fullscreen: hrule.MatchFullscreen, - Pinned: hrule.MatchPinned, - Initialised: hrule.MatchInitialised, - }, - } - applyHyprlandRuleAction(&wr.Actions, hrule.Rule, hrule.Value) - - if wr.ID == "" { - wr.ID = hrule.MatchClass - if wr.ID == "" { - wr.ID = hrule.MatchTitle - } - } - - rules = append(rules, wr) - currentID = "" - currentName = "" - } - } - - return rules, nil -} - -func (p *HyprlandWritableProvider) 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.formatRuleLines(rule)...) - } - - return os.WriteFile(rulesPath, []byte(strings.Join(lines, "\n")), 0644) -} - -func (p *HyprlandWritableProvider) formatRuleLines(rule windowrules.WindowRule) []string { - var lines []string - lines = append(lines, fmt.Sprintf("# DMS-RULE: id=%s, name=%s", rule.ID, rule.Name)) - - var matchParts []string - if rule.MatchCriteria.AppID != "" { - matchParts = append(matchParts, fmt.Sprintf("class:%s", rule.MatchCriteria.AppID)) - } - if rule.MatchCriteria.Title != "" { - matchParts = append(matchParts, fmt.Sprintf("title:%s", rule.MatchCriteria.Title)) - } - if rule.MatchCriteria.XWayland != nil { - matchParts = append(matchParts, fmt.Sprintf("xwayland:%d", boolToInt(*rule.MatchCriteria.XWayland))) - } - if rule.MatchCriteria.IsFloating != nil { - matchParts = append(matchParts, fmt.Sprintf("floating:%d", boolToInt(*rule.MatchCriteria.IsFloating))) - } - if rule.MatchCriteria.Fullscreen != nil { - matchParts = append(matchParts, fmt.Sprintf("fullscreen:%d", boolToInt(*rule.MatchCriteria.Fullscreen))) - } - if rule.MatchCriteria.Pinned != nil { - matchParts = append(matchParts, fmt.Sprintf("pinned:%d", boolToInt(*rule.MatchCriteria.Pinned))) - } - - matchStr := strings.Join(matchParts, ", ") - a := rule.Actions - - if a.OpenFloating != nil && *a.OpenFloating { - lines = append(lines, fmt.Sprintf("windowrulev2 = float, %s", matchStr)) - } - if a.Tile != nil && *a.Tile { - lines = append(lines, fmt.Sprintf("windowrulev2 = tile, %s", matchStr)) - } - if a.OpenFullscreen != nil && *a.OpenFullscreen { - lines = append(lines, fmt.Sprintf("windowrulev2 = fullscreen, %s", matchStr)) - } - if a.OpenMaximized != nil && *a.OpenMaximized { - lines = append(lines, fmt.Sprintf("windowrulev2 = maximize, %s", matchStr)) - } - if a.NoFocus != nil && *a.NoFocus { - lines = append(lines, fmt.Sprintf("windowrulev2 = nofocus, %s", matchStr)) - } - if a.NoBorder != nil && *a.NoBorder { - lines = append(lines, fmt.Sprintf("windowrulev2 = noborder, %s", matchStr)) - } - if a.NoShadow != nil && *a.NoShadow { - lines = append(lines, fmt.Sprintf("windowrulev2 = noshadow, %s", matchStr)) - } - if a.NoDim != nil && *a.NoDim { - lines = append(lines, fmt.Sprintf("windowrulev2 = nodim, %s", matchStr)) - } - if a.NoBlur != nil && *a.NoBlur { - lines = append(lines, fmt.Sprintf("windowrulev2 = noblur, %s", matchStr)) - } - if a.NoAnim != nil && *a.NoAnim { - lines = append(lines, fmt.Sprintf("windowrulev2 = noanim, %s", matchStr)) - } - if a.NoRounding != nil && *a.NoRounding { - lines = append(lines, fmt.Sprintf("windowrulev2 = norounding, %s", matchStr)) - } - if a.Pin != nil && *a.Pin { - lines = append(lines, fmt.Sprintf("windowrulev2 = pin, %s", matchStr)) - } - if a.Opaque != nil && *a.Opaque { - lines = append(lines, fmt.Sprintf("windowrulev2 = opaque, %s", matchStr)) - } - if a.ForcergbX != nil && *a.ForcergbX { - lines = append(lines, fmt.Sprintf("windowrulev2 = forcergbx, %s", matchStr)) - } - if a.Opacity != nil { - lines = append(lines, fmt.Sprintf("windowrulev2 = opacity %.2f, %s", *a.Opacity, matchStr)) - } - if a.Size != "" { - lines = append(lines, fmt.Sprintf("windowrulev2 = size %s, %s", a.Size, matchStr)) - } - if a.Move != "" { - lines = append(lines, fmt.Sprintf("windowrulev2 = move %s, %s", a.Move, matchStr)) - } - if a.Monitor != "" { - lines = append(lines, fmt.Sprintf("windowrulev2 = monitor %s, %s", a.Monitor, matchStr)) - } - if a.Workspace != "" { - lines = append(lines, fmt.Sprintf("windowrulev2 = workspace %s, %s", a.Workspace, matchStr)) - } - if a.CornerRadius != nil { - lines = append(lines, fmt.Sprintf("windowrulev2 = rounding %d, %s", *a.CornerRadius, matchStr)) - } - if a.Idleinhibit != "" { - lines = append(lines, fmt.Sprintf("windowrulev2 = idleinhibit %s, %s", a.Idleinhibit, matchStr)) - } - - if len(lines) == 1 { - lines = append(lines, fmt.Sprintf("# (no actions defined for rule %s)", rule.ID)) - } - - lines = append(lines, "") - return lines -} - -func boolToInt(b bool) int { - if b { - return 1 - } - return 0 -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser_test.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser_test.go deleted file mode 100644 index 5341b4b..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/hyprland_parser_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestParseWindowRuleV1(t *testing.T) { - parser := NewHyprlandRulesParser("") - - tests := []struct { - name string - line string - wantClass string - wantRule string - wantNil bool - }{ - { - name: "basic float rule", - line: "windowrule = float, ^(firefox)$", - wantClass: "^(firefox)$", - wantRule: "float", - }, - { - name: "tile rule", - line: "windowrule = tile, steam", - wantClass: "steam", - wantRule: "tile", - }, - { - name: "no match returns empty class", - line: "windowrule = float", - wantClass: "", - wantRule: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parser.parseWindowRuleLine(tt.line) - if tt.wantNil { - if result != nil { - t.Errorf("expected nil, got %+v", result) - } - return - } - if result == nil { - t.Fatal("expected non-nil result") - } - if result.MatchClass != tt.wantClass { - t.Errorf("MatchClass = %q, want %q", result.MatchClass, tt.wantClass) - } - if result.Rule != tt.wantRule { - t.Errorf("Rule = %q, want %q", result.Rule, tt.wantRule) - } - }) - } -} - -func TestParseWindowRuleV2(t *testing.T) { - parser := NewHyprlandRulesParser("") - - tests := []struct { - name string - line string - wantClass string - wantTitle string - wantRule string - wantValue string - }{ - { - name: "float with class", - line: "windowrulev2 = float, class:^(firefox)$", - wantClass: "^(firefox)$", - wantRule: "float", - }, - { - name: "opacity with value", - line: "windowrulev2 = opacity 0.8, class:^(code)$", - wantClass: "^(code)$", - wantRule: "opacity", - wantValue: "0.8", - }, - { - name: "size with value and title", - line: "windowrulev2 = size 800 600, class:^(steam)$, title:Settings", - wantClass: "^(steam)$", - wantTitle: "Settings", - wantRule: "size", - wantValue: "800 600", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parser.parseWindowRuleLine(tt.line) - if result == nil { - t.Fatal("expected non-nil result") - } - if result.MatchClass != tt.wantClass { - t.Errorf("MatchClass = %q, want %q", result.MatchClass, tt.wantClass) - } - if result.MatchTitle != tt.wantTitle { - t.Errorf("MatchTitle = %q, want %q", result.MatchTitle, tt.wantTitle) - } - if result.Rule != tt.wantRule { - t.Errorf("Rule = %q, want %q", result.Rule, tt.wantRule) - } - if result.Value != tt.wantValue { - t.Errorf("Value = %q, want %q", result.Value, tt.wantValue) - } - }) - } -} - -func TestConvertHyprlandRulesToWindowRules(t *testing.T) { - hyprRules := []HyprlandWindowRule{ - {MatchClass: "^(firefox)$", Rule: "float"}, - {MatchClass: "^(code)$", Rule: "opacity", Value: "0.9"}, - {MatchClass: "^(steam)$", Rule: "maximize"}, - } - - result := ConvertHyprlandRulesToWindowRules(hyprRules) - - if len(result) != 3 { - t.Errorf("expected 3 rules, got %d", len(result)) - } - - if result[0].MatchCriteria.AppID != "^(firefox)$" { - t.Errorf("rule 0 AppID = %q, want ^(firefox)$", result[0].MatchCriteria.AppID) - } - if result[0].Actions.OpenFloating == nil || !*result[0].Actions.OpenFloating { - t.Error("rule 0 should have OpenFloating = true") - } - - if result[1].Actions.Opacity == nil || *result[1].Actions.Opacity != 0.9 { - t.Errorf("rule 1 Opacity = %v, want 0.9", result[1].Actions.Opacity) - } - - if result[2].Actions.OpenMaximized == nil || !*result[2].Actions.OpenMaximized { - t.Error("rule 2 should have OpenMaximized = true") - } -} - -func TestHyprlandWritableProvider(t *testing.T) { - tmpDir := t.TempDir() - provider := NewHyprlandWritableProvider(tmpDir) - - if provider.Name() != "hyprland" { - t.Errorf("Name() = %q, want hyprland", provider.Name()) - } - - expectedPath := filepath.Join(tmpDir, "dms", "windowrules.conf") - if provider.GetOverridePath() != expectedPath { - t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath) - } -} - -func TestHyprlandSetAndLoadDMSRules(t *testing.T) { - tmpDir := t.TempDir() - provider := NewHyprlandWritableProvider(tmpDir) - - rule := newTestWindowRule("test_id", "Test Rule", "^(firefox)$") - rule.Actions.OpenFloating = boolPtr(true) - - if err := provider.SetRule(rule); err != nil { - t.Fatalf("SetRule failed: %v", err) - } - - rules, err := provider.LoadDMSRules() - if err != nil { - t.Fatalf("LoadDMSRules failed: %v", err) - } - - if len(rules) != 1 { - t.Fatalf("expected 1 rule, got %d", len(rules)) - } - - if rules[0].ID != "test_id" { - t.Errorf("ID = %q, want test_id", rules[0].ID) - } - if rules[0].MatchCriteria.AppID != "^(firefox)$" { - t.Errorf("AppID = %q, want ^(firefox)$", rules[0].MatchCriteria.AppID) - } -} - -func TestHyprlandRemoveRule(t *testing.T) { - tmpDir := t.TempDir() - provider := NewHyprlandWritableProvider(tmpDir) - - rule1 := newTestWindowRule("rule1", "Rule 1", "^(app1)$") - rule1.Actions.OpenFloating = boolPtr(true) - rule2 := newTestWindowRule("rule2", "Rule 2", "^(app2)$") - rule2.Actions.OpenFloating = boolPtr(true) - - _ = provider.SetRule(rule1) - _ = provider.SetRule(rule2) - - if err := provider.RemoveRule("rule1"); err != nil { - t.Fatalf("RemoveRule failed: %v", err) - } - - rules, _ := provider.LoadDMSRules() - if len(rules) != 1 { - t.Fatalf("expected 1 rule after removal, got %d", len(rules)) - } - if rules[0].ID != "rule2" { - t.Errorf("remaining rule ID = %q, want rule2", rules[0].ID) - } -} - -func TestHyprlandReorderRules(t *testing.T) { - tmpDir := t.TempDir() - provider := NewHyprlandWritableProvider(tmpDir) - - rule1 := newTestWindowRule("rule1", "Rule 1", "^(app1)$") - rule1.Actions.OpenFloating = boolPtr(true) - rule2 := newTestWindowRule("rule2", "Rule 2", "^(app2)$") - rule2.Actions.OpenFloating = boolPtr(true) - rule3 := newTestWindowRule("rule3", "Rule 3", "^(app3)$") - rule3.Actions.OpenFloating = boolPtr(true) - - _ = provider.SetRule(rule1) - _ = provider.SetRule(rule2) - _ = provider.SetRule(rule3) - - if err := provider.ReorderRules([]string{"rule3", "rule1", "rule2"}); err != nil { - t.Fatalf("ReorderRules failed: %v", err) - } - - rules, _ := provider.LoadDMSRules() - if len(rules) != 3 { - t.Fatalf("expected 3 rules, got %d", len(rules)) - } - expectedOrder := []string{"rule3", "rule1", "rule2"} - for i, expectedID := range expectedOrder { - if rules[i].ID != expectedID { - t.Errorf("rule %d ID = %q, want %q", i, rules[i].ID, expectedID) - } - } -} - -func TestHyprlandParseConfigWithSource(t *testing.T) { - tmpDir := t.TempDir() - - mainConfig := ` -windowrulev2 = float, class:^(mainapp)$ -source = ./extra.conf -` - extraConfig := ` -windowrulev2 = tile, class:^(extraapp)$ -` - - if err := os.WriteFile(filepath.Join(tmpDir, "hyprland.conf"), []byte(mainConfig), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(tmpDir, "extra.conf"), []byte(extraConfig), 0644); err != nil { - t.Fatal(err) - } - - parser := NewHyprlandRulesParser(tmpDir) - rules, err := parser.Parse() - if err != nil { - t.Fatalf("Parse failed: %v", err) - } - - if len(rules) != 2 { - t.Errorf("expected 2 rules, got %d", len(rules)) - } -} - -func TestBoolToInt(t *testing.T) { - if boolToInt(true) != 1 { - t.Error("boolToInt(true) should be 1") - } - if boolToInt(false) != 0 { - t.Error("boolToInt(false) should be 0") - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser.go deleted file mode 100644 index 5bb5ec4..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser.go +++ /dev/null @@ -1,875 +0,0 @@ -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 { %s %s; }", name, parts[0], parts[1]) - } - // Bare number without type prefix — default to "fixed" - if _, err := strconv.Atoi(value); err == nil { - return fmt.Sprintf(" %s { fixed %s; }", name, value) - } - return fmt.Sprintf(" %s { }", name) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser_test.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser_test.go deleted file mode 100644 index f764b6a..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/niri_parser_test.go +++ /dev/null @@ -1,335 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestNiriParseBasicWindowRule(t *testing.T) { - tmpDir := t.TempDir() - - config := ` -window-rule { - match app-id="^firefox$" - opacity 0.9 - open-floating true -} -` - if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil { - t.Fatal(err) - } - - parser := NewNiriRulesParser(tmpDir) - rules, err := parser.Parse() - if err != nil { - t.Fatalf("Parse failed: %v", err) - } - - if len(rules) != 1 { - t.Fatalf("expected 1 rule, got %d", len(rules)) - } - - rule := rules[0] - if rule.MatchAppID != "^firefox$" { - t.Errorf("MatchAppID = %q, want ^firefox$", rule.MatchAppID) - } - if rule.Opacity == nil || *rule.Opacity != 0.9 { - t.Errorf("Opacity = %v, want 0.9", rule.Opacity) - } - if rule.OpenFloating == nil || !*rule.OpenFloating { - t.Error("OpenFloating should be true") - } -} - -func TestNiriParseMultipleRules(t *testing.T) { - tmpDir := t.TempDir() - - config := ` -window-rule { - match app-id="app1" - open-maximized true -} - -window-rule { - match app-id="app2" - open-fullscreen true -} -` - if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil { - t.Fatal(err) - } - - parser := NewNiriRulesParser(tmpDir) - rules, err := parser.Parse() - if err != nil { - t.Fatalf("Parse failed: %v", err) - } - - if len(rules) != 2 { - t.Fatalf("expected 2 rules, got %d", len(rules)) - } - - if rules[0].MatchAppID != "app1" { - t.Errorf("rule 0 MatchAppID = %q, want app1", rules[0].MatchAppID) - } - if rules[1].MatchAppID != "app2" { - t.Errorf("rule 1 MatchAppID = %q, want app2", rules[1].MatchAppID) - } -} - -func TestConvertNiriRulesToWindowRules(t *testing.T) { - niriRules := []NiriWindowRule{ - {MatchAppID: "^firefox$", Opacity: floatPtr(0.8)}, - {MatchAppID: "^code$", OpenFloating: boolPtr(true)}, - } - - result := ConvertNiriRulesToWindowRules(niriRules) - - if len(result) != 2 { - t.Errorf("expected 2 rules, got %d", len(result)) - } - - if result[0].MatchCriteria.AppID != "^firefox$" { - t.Errorf("rule 0 AppID = %q, want ^firefox$", result[0].MatchCriteria.AppID) - } - if result[0].Actions.Opacity == nil || *result[0].Actions.Opacity != 0.8 { - t.Errorf("rule 0 Opacity = %v, want 0.8", result[0].Actions.Opacity) - } - - if result[1].Actions.OpenFloating == nil || !*result[1].Actions.OpenFloating { - t.Error("rule 1 should have OpenFloating = true") - } -} - -func TestNiriWritableProvider(t *testing.T) { - tmpDir := t.TempDir() - provider := NewNiriWritableProvider(tmpDir) - - if provider.Name() != "niri" { - t.Errorf("Name() = %q, want niri", provider.Name()) - } - - expectedPath := filepath.Join(tmpDir, "dms", "windowrules.kdl") - if provider.GetOverridePath() != expectedPath { - t.Errorf("GetOverridePath() = %q, want %q", provider.GetOverridePath(), expectedPath) - } -} - -func TestNiriSetAndLoadDMSRules(t *testing.T) { - tmpDir := t.TempDir() - provider := NewNiriWritableProvider(tmpDir) - - rule := newTestWindowRule("test_id", "Test Rule", "^firefox$") - rule.Actions.OpenFloating = boolPtr(true) - rule.Actions.Opacity = floatPtr(0.85) - - if err := provider.SetRule(rule); err != nil { - t.Fatalf("SetRule failed: %v", err) - } - - rules, err := provider.LoadDMSRules() - if err != nil { - t.Fatalf("LoadDMSRules failed: %v", err) - } - - if len(rules) != 1 { - t.Fatalf("expected 1 rule, got %d", len(rules)) - } - - if rules[0].ID != "test_id" { - t.Errorf("ID = %q, want test_id", rules[0].ID) - } - if rules[0].MatchCriteria.AppID != "^firefox$" { - t.Errorf("AppID = %q, want ^firefox$", rules[0].MatchCriteria.AppID) - } -} - -func TestNiriRemoveRule(t *testing.T) { - tmpDir := t.TempDir() - provider := NewNiriWritableProvider(tmpDir) - - rule1 := newTestWindowRule("rule1", "Rule 1", "app1") - rule1.Actions.OpenFloating = boolPtr(true) - rule2 := newTestWindowRule("rule2", "Rule 2", "app2") - rule2.Actions.OpenFloating = boolPtr(true) - - _ = provider.SetRule(rule1) - _ = provider.SetRule(rule2) - - if err := provider.RemoveRule("rule1"); err != nil { - t.Fatalf("RemoveRule failed: %v", err) - } - - rules, _ := provider.LoadDMSRules() - if len(rules) != 1 { - t.Fatalf("expected 1 rule after removal, got %d", len(rules)) - } - if rules[0].ID != "rule2" { - t.Errorf("remaining rule ID = %q, want rule2", rules[0].ID) - } -} - -func TestNiriReorderRules(t *testing.T) { - tmpDir := t.TempDir() - provider := NewNiriWritableProvider(tmpDir) - - rule1 := newTestWindowRule("rule1", "Rule 1", "app1") - rule1.Actions.OpenFloating = boolPtr(true) - rule2 := newTestWindowRule("rule2", "Rule 2", "app2") - rule2.Actions.OpenFloating = boolPtr(true) - rule3 := newTestWindowRule("rule3", "Rule 3", "app3") - rule3.Actions.OpenFloating = boolPtr(true) - - _ = provider.SetRule(rule1) - _ = provider.SetRule(rule2) - _ = provider.SetRule(rule3) - - if err := provider.ReorderRules([]string{"rule3", "rule1", "rule2"}); err != nil { - t.Fatalf("ReorderRules failed: %v", err) - } - - rules, _ := provider.LoadDMSRules() - if len(rules) != 3 { - t.Fatalf("expected 3 rules, got %d", len(rules)) - } - expectedOrder := []string{"rule3", "rule1", "rule2"} - for i, expectedID := range expectedOrder { - if rules[i].ID != expectedID { - t.Errorf("rule %d ID = %q, want %q", i, rules[i].ID, expectedID) - } - } -} - -func TestNiriParseConfigWithInclude(t *testing.T) { - tmpDir := t.TempDir() - - mainConfig := ` -window-rule { - match app-id="mainapp" - opacity 1.0 -} - -include "extra.kdl" -` - extraConfig := ` -window-rule { - match app-id="extraapp" - open-maximized true -} -` - - if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(mainConfig), 0644); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filepath.Join(tmpDir, "extra.kdl"), []byte(extraConfig), 0644); err != nil { - t.Fatal(err) - } - - parser := NewNiriRulesParser(tmpDir) - rules, err := parser.Parse() - if err != nil { - t.Fatalf("Parse failed: %v", err) - } - - if len(rules) != 2 { - t.Errorf("expected 2 rules, got %d", len(rules)) - } -} - -func TestNiriParseSizeNode(t *testing.T) { - tmpDir := t.TempDir() - - config := ` -window-rule { - match app-id="testapp" - default-column-width { fixed 800; } - default-window-height { proportion 0.5; } -} -` - if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil { - t.Fatal(err) - } - - parser := NewNiriRulesParser(tmpDir) - rules, err := parser.Parse() - if err != nil { - t.Fatalf("Parse failed: %v", err) - } - - if len(rules) != 1 { - t.Fatalf("expected 1 rule, got %d", len(rules)) - } - - if rules[0].DefaultColumnWidth != "fixed 800" { - t.Errorf("DefaultColumnWidth = %q, want 'fixed 800'", rules[0].DefaultColumnWidth) - } - if rules[0].DefaultWindowHeight != "proportion 0.5" { - t.Errorf("DefaultWindowHeight = %q, want 'proportion 0.5'", rules[0].DefaultWindowHeight) - } -} - -func TestFormatSizeProperty(t *testing.T) { - tests := []struct { - name string - propName string - value string - want string - }{ - { - name: "fixed size", - propName: "default-column-width", - value: "fixed 800", - want: " default-column-width { fixed 800; }", - }, - { - name: "proportion", - propName: "default-window-height", - value: "proportion 0.5", - want: " default-window-height { proportion 0.5; }", - }, - { - name: "invalid format", - propName: "default-column-width", - value: "invalid", - want: " default-column-width { }", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := formatSizeProperty(tt.propName, tt.value) - if result != tt.want { - t.Errorf("formatSizeProperty(%q, %q) = %q, want %q", - tt.propName, tt.value, result, tt.want) - } - }) - } -} - -func TestNiriDMSRulesStatus(t *testing.T) { - tmpDir := t.TempDir() - - config := ` -window-rule { - match app-id="testapp" - opacity 0.9 -} -` - if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0644); err != nil { - t.Fatal(err) - } - - result, err := ParseNiriWindowRules(tmpDir) - if err != nil { - t.Fatalf("ParseNiriWindowRules failed: %v", err) - } - - if result.DMSStatus == nil { - t.Fatal("DMSStatus should not be nil") - } - - if result.DMSStatus.Exists { - t.Error("DMSStatus.Exists should be false when dms rules file doesn't exist") - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/providers_test.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/providers_test.go deleted file mode 100644 index de84a38..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/providers/providers_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package providers - -import "github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules" - -func newTestWindowRule(id, name, appID string) windowrules.WindowRule { - return windowrules.WindowRule{ - ID: id, - Name: name, - Enabled: true, - MatchCriteria: windowrules.MatchCriteria{ - AppID: appID, - }, - } -} - -func boolPtr(b bool) *bool { - return &b -} - -func floatPtr(f float64) *float64 { - return &f -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/types.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/types.go deleted file mode 100644 index 556acb7..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/internal/windowrules/types.go +++ /dev/null @@ -1,103 +0,0 @@ -package windowrules - -type MatchCriteria struct { - AppID string `json:"appId,omitempty"` - Title string `json:"title,omitempty"` - IsFloating *bool `json:"isFloating,omitempty"` - IsActive *bool `json:"isActive,omitempty"` - IsFocused *bool `json:"isFocused,omitempty"` - IsActiveInColumn *bool `json:"isActiveInColumn,omitempty"` - IsWindowCastTarget *bool `json:"isWindowCastTarget,omitempty"` - IsUrgent *bool `json:"isUrgent,omitempty"` - AtStartup *bool `json:"atStartup,omitempty"` - XWayland *bool `json:"xwayland,omitempty"` - Fullscreen *bool `json:"fullscreen,omitempty"` - Pinned *bool `json:"pinned,omitempty"` - Initialised *bool `json:"initialised,omitempty"` -} - -type Actions struct { - Opacity *float64 `json:"opacity,omitempty"` - OpenFloating *bool `json:"openFloating,omitempty"` - OpenMaximized *bool `json:"openMaximized,omitempty"` - OpenMaximizedToEdges *bool `json:"openMaximizedToEdges,omitempty"` - OpenFullscreen *bool `json:"openFullscreen,omitempty"` - OpenFocused *bool `json:"openFocused,omitempty"` - OpenOnOutput string `json:"openOnOutput,omitempty"` - OpenOnWorkspace string `json:"openOnWorkspace,omitempty"` - DefaultColumnWidth string `json:"defaultColumnWidth,omitempty"` - DefaultWindowHeight string `json:"defaultWindowHeight,omitempty"` - VariableRefreshRate *bool `json:"variableRefreshRate,omitempty"` - BlockOutFrom string `json:"blockOutFrom,omitempty"` - DefaultColumnDisplay string `json:"defaultColumnDisplay,omitempty"` - ScrollFactor *float64 `json:"scrollFactor,omitempty"` - CornerRadius *int `json:"cornerRadius,omitempty"` - ClipToGeometry *bool `json:"clipToGeometry,omitempty"` - TiledState *bool `json:"tiledState,omitempty"` - MinWidth *int `json:"minWidth,omitempty"` - MaxWidth *int `json:"maxWidth,omitempty"` - MinHeight *int `json:"minHeight,omitempty"` - MaxHeight *int `json:"maxHeight,omitempty"` - BorderColor string `json:"borderColor,omitempty"` - FocusRingColor string `json:"focusRingColor,omitempty"` - FocusRingOff *bool `json:"focusRingOff,omitempty"` - BorderOff *bool `json:"borderOff,omitempty"` - DrawBorderWithBg *bool `json:"drawBorderWithBackground,omitempty"` - Size string `json:"size,omitempty"` - Move string `json:"move,omitempty"` - Monitor string `json:"monitor,omitempty"` - Workspace string `json:"workspace,omitempty"` - Tile *bool `json:"tile,omitempty"` - NoFocus *bool `json:"nofocus,omitempty"` - NoBorder *bool `json:"noborder,omitempty"` - NoShadow *bool `json:"noshadow,omitempty"` - NoDim *bool `json:"nodim,omitempty"` - NoBlur *bool `json:"noblur,omitempty"` - NoAnim *bool `json:"noanim,omitempty"` - NoRounding *bool `json:"norounding,omitempty"` - Pin *bool `json:"pin,omitempty"` - Opaque *bool `json:"opaque,omitempty"` - ForcergbX *bool `json:"forcergbx,omitempty"` - Idleinhibit string `json:"idleinhibit,omitempty"` -} - -type WindowRule struct { - ID string `json:"id"` - Name string `json:"name,omitempty"` - Enabled bool `json:"enabled"` - MatchCriteria MatchCriteria `json:"matchCriteria"` - Actions Actions `json:"actions"` - Source string `json:"source,omitempty"` -} - -type DMSRulesStatus struct { - Exists bool `json:"exists"` - Included bool `json:"included"` - IncludePosition int `json:"includePosition"` - TotalIncludes int `json:"totalIncludes"` - RulesAfterDMS int `json:"rulesAfterDms"` - Effective bool `json:"effective"` - OverriddenBy int `json:"overriddenBy"` - StatusMessage string `json:"statusMessage"` -} - -type RuleSet struct { - Title string `json:"title"` - Provider string `json:"provider"` - Rules []WindowRule `json:"rules"` - DMSRulesIncluded bool `json:"dmsRulesIncluded"` - DMSStatus *DMSRulesStatus `json:"dmsStatus,omitempty"` -} - -type Provider interface { - Name() string - GetRuleSet() (*RuleSet, error) -} - -type WritableProvider interface { - Provider - SetRule(rule WindowRule) error - RemoveRule(id string) error - ReorderRules(ids []string) error - GetOverridePath() string -} -- cgit v1.3