From 6e7aca8b15621558026f55d43ed07d213a706e3f Mon Sep 17 00:00:00 2001 From: purian23 Date: Wed, 3 Jun 2026 19:43:23 -0400 Subject: [PATCH] feat(window-rules): add niri default-floating-position rule - Closes #2018 --- .../windowrules/providers/niri_parser.go | 158 +++++++++++------- core/internal/windowrules/types.go | 36 ++-- quickshell/Modals/WindowRuleModal.qml | 113 ++++++++++++- .../Modules/Settings/WindowRulesTab.qml | 3 + 4 files changed, 233 insertions(+), 77 deletions(-) diff --git a/core/internal/windowrules/providers/niri_parser.go b/core/internal/windowrules/providers/niri_parser.go index 5b930741..6c7423f8 100644 --- a/core/internal/windowrules/providers/niri_parser.go +++ b/core/internal/windowrules/providers/niri_parser.go @@ -67,6 +67,9 @@ type NiriWindowRule struct { BgXray *bool BgNoise *float64 BgSaturation *float64 + DefaultFloatingX *int + DefaultFloatingY *int + DefaultFloatingRelative string Source string } @@ -316,6 +319,8 @@ func (p *NiriRulesParser) parseWindowRuleNode(node *document.Node) { rule.DrawBorderWithBg = &b case "background-effect": p.parseBackgroundEffectNode(child, &rule) + case "default-floating-position": + p.parseFloatingPositionNode(child, &rule) } } @@ -444,6 +449,25 @@ func (p *NiriRulesParser) parseBackgroundEffectNode(node *document.Node, rule *N } } +func (p *NiriRulesParser) parseFloatingPositionNode(node *document.Node, rule *NiriWindowRule) { + if node.Properties == nil { + return + } + if val, ok := node.Properties.Get("x"); ok { + if n, err := strconv.Atoi(strings.TrimSpace(val.ValueString())); err == nil { + rule.DefaultFloatingX = &n + } + } + if val, ok := node.Properties.Get("y"); ok { + if n, err := strconv.Atoi(strings.TrimSpace(val.ValueString())); err == nil { + rule.DefaultFloatingY = &n + } + } + if val, ok := node.Properties.Get("relative-to"); ok { + rule.DefaultFloatingRelative = val.ValueString() + } +} + func (p *NiriRulesParser) parseFloatArg(node *document.Node) (float64, bool) { if len(node.Arguments) == 0 { return 0, false @@ -575,36 +599,39 @@ func ConvertNiriRulesToWindowRules(niriRules []NiriWindowRule) []windowrules.Win }, Matches: convertNiriMatches(nr.Matches), 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, - BackgroundBlur: nr.BgBlur, - BackgroundXray: nr.BgXray, - BackgroundNoise: nr.BgNoise, - BackgroundSaturation: nr.BgSaturation, + 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, + BackgroundBlur: nr.BgBlur, + BackgroundXray: nr.BgXray, + BackgroundNoise: nr.BgNoise, + BackgroundSaturation: nr.BgSaturation, + DefaultFloatingX: nr.DefaultFloatingX, + DefaultFloatingY: nr.DefaultFloatingY, + DefaultFloatingRelativeTo: nr.DefaultFloatingRelative, }, } result = append(result, wr) @@ -785,36 +812,39 @@ func (p *NiriWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) }, Matches: convertNiriMatches(nr.Matches), 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, - BackgroundBlur: nr.BgBlur, - BackgroundXray: nr.BgXray, - BackgroundNoise: nr.BgNoise, - BackgroundSaturation: nr.BgSaturation, + 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, + BackgroundBlur: nr.BgBlur, + BackgroundXray: nr.BgXray, + BackgroundNoise: nr.BgNoise, + BackgroundSaturation: nr.BgSaturation, + DefaultFloatingX: nr.DefaultFloatingX, + DefaultFloatingY: nr.DefaultFloatingY, + DefaultFloatingRelativeTo: nr.DefaultFloatingRelative, }, } @@ -989,6 +1019,14 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string { lines = append(lines, " }") } + if a.DefaultFloatingX != nil && a.DefaultFloatingY != nil { + line := fmt.Sprintf(" default-floating-position x=%d y=%d", *a.DefaultFloatingX, *a.DefaultFloatingY) + if a.DefaultFloatingRelativeTo != "" { + line += fmt.Sprintf(" relative-to=%q", a.DefaultFloatingRelativeTo) + } + lines = append(lines, line) + } + lines = append(lines, "}") return strings.Join(lines, "\n") } diff --git a/core/internal/windowrules/types.go b/core/internal/windowrules/types.go index d6b60f99..5d3fdb71 100644 --- a/core/internal/windowrules/types.go +++ b/core/internal/windowrules/types.go @@ -47,22 +47,26 @@ type Actions struct { BackgroundXray *bool `json:"backgroundXray,omitempty"` BackgroundNoise *float64 `json:"backgroundNoise,omitempty"` BackgroundSaturation *float64 `json:"backgroundSaturation,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"` + + DefaultFloatingX *int `json:"defaultFloatingX,omitempty"` + DefaultFloatingY *int `json:"defaultFloatingY,omitempty"` + DefaultFloatingRelativeTo string `json:"defaultFloatingRelativeTo,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 { diff --git a/quickshell/Modals/WindowRuleModal.qml b/quickshell/Modals/WindowRuleModal.qml index b5f67118..3abcbb52 100644 --- a/quickshell/Modals/WindowRuleModal.qml +++ b/quickshell/Modals/WindowRuleModal.qml @@ -74,6 +74,9 @@ FloatingWindow { noiseSlider.value = 5; saturationEnabled.checked = false; saturationSlider.value = 100; + floatingXInput.text = ""; + floatingYInput.text = ""; + floatingRelativeDropdown.currentValue = "top-left"; minWidthInput.text = ""; maxWidthInput.text = ""; minHeightInput.text = ""; @@ -183,6 +186,10 @@ FloatingWindow { saturationEnabled.checked = hasSaturation; saturationSlider.value = hasSaturation ? Math.round(actions.backgroundSaturation * 100) : 100; + floatingXInput.text = (actions.defaultFloatingX !== undefined && actions.defaultFloatingX !== null) ? String(actions.defaultFloatingX) : ""; + floatingYInput.text = (actions.defaultFloatingY !== undefined && actions.defaultFloatingY !== null) ? String(actions.defaultFloatingY) : ""; + floatingRelativeDropdown.currentValue = actions.defaultFloatingRelativeTo || "top-left"; + minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : ""; maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : ""; minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : ""; @@ -327,6 +334,15 @@ FloatingWindow { if (saturationEnabled.checked && isNiri) actions.backgroundSaturation = saturationSlider.value / 100; + const floatX = parseInt(floatingXInput.text); + const floatY = parseInt(floatingYInput.text); + if (isNiri && !isNaN(floatX) && !isNaN(floatY)) { + actions.defaultFloatingX = floatX; + actions.defaultFloatingY = floatY; + if (floatingRelativeDropdown.currentValue && floatingRelativeDropdown.currentValue !== "top-left") + actions.defaultFloatingRelativeTo = floatingRelativeDropdown.currentValue; + } + const minW = parseInt(minWidthInput.text); const maxW = parseInt(maxWidthInput.text); const minH = parseInt(minHeightInput.text); @@ -496,7 +512,7 @@ FloatingWindow { id: mc property string label: "" property int triState: 0 - property string unsetLabel: I18n.tr("Any") + property string unsetLabel: I18n.tr("Default") property bool readOnly: false readonly property var stateText: [mc.unsetLabel, "true", "false"] readonly property var stateColor: [Theme.surfaceVariantText, Theme.primary, Theme.error] @@ -1239,6 +1255,101 @@ FloatingWindow { } } + SectionHeader { + title: I18n.tr("Floating Position") + visible: isNiri + } + + StyledText { + width: parent.width + visible: isNiri + text: I18n.tr("Initial position for floating windows. Set both X and Y; anchor controls which corner/edge they're relative to.") + font.pixelSize: Theme.fontSizeSmall - 1 + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: isNiri + + Column { + width: (parent.width - Theme.spacingM * 2) / 3 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("X") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + InputField { + width: parent.width + hasFocus: floatingXInput.activeFocus + DankTextField { + id: floatingXInput + anchors.fill: parent + font.pixelSize: Theme.fontSizeSmall + textColor: Theme.surfaceText + placeholderText: "px" + backgroundColor: "transparent" + enabled: root.visible + } + } + } + + Column { + width: (parent.width - Theme.spacingM * 2) / 3 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Y") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + InputField { + width: parent.width + hasFocus: floatingYInput.activeFocus + DankTextField { + id: floatingYInput + anchors.fill: parent + font.pixelSize: Theme.fontSizeSmall + textColor: Theme.surfaceText + placeholderText: "px" + backgroundColor: "transparent" + enabled: root.visible + } + } + } + + Column { + width: (parent.width - Theme.spacingM * 2) / 3 + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Anchor") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + DankDropdown { + id: floatingRelativeDropdown + width: parent.width + dropdownWidth: parent.width + compactMode: true + options: ["top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right"] + } + } + } + SectionHeader { title: I18n.tr("Size Constraints") } diff --git a/quickshell/Modules/Settings/WindowRulesTab.qml b/quickshell/Modules/Settings/WindowRulesTab.qml index 6974d748..a6dadf4d 100644 --- a/quickshell/Modules/Settings/WindowRulesTab.qml +++ b/quickshell/Modules/Settings/WindowRulesTab.qml @@ -121,6 +121,9 @@ Item { "backgroundXray": I18n.tr("X-Ray"), "backgroundNoise": I18n.tr("Noise"), "backgroundSaturation": I18n.tr("Saturation"), + "defaultFloatingX": I18n.tr("Float X"), + "defaultFloatingY": I18n.tr("Float Y"), + "defaultFloatingRelativeTo": I18n.tr("Float Anchor"), "borderColor": I18n.tr("Border Color"), "focusRingColor": I18n.tr("Focus Ring Color"), "focusRingOff": I18n.tr("Focus Ring Off"),