300 lines
9.4 KiB
Python
300 lines
9.4 KiB
Python
"""Helpers for Niri global window effect rules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from nirimod.kdl_parser import KdlNode, remove_child, set_child_arg, set_node_flag
|
|
|
|
_RULE_CHILD_ORDER = [
|
|
"match",
|
|
"exclude",
|
|
"geometry-corner-radius",
|
|
"clip-to-geometry",
|
|
"draw-border-with-background",
|
|
"opacity",
|
|
"background-effect",
|
|
]
|
|
|
|
_EFFECT_CHILD_ORDER = ["blur", "xray"]
|
|
_BLUR_CONFIG_CHILD_ORDER = ["off", "passes", "offset", "noise", "saturation"]
|
|
|
|
|
|
def _is_global_window_rule(node: KdlNode) -> bool:
|
|
return (
|
|
node.name == "window-rule"
|
|
and not node.get_children("match")
|
|
and not node.get_children("exclude")
|
|
)
|
|
|
|
|
|
def _is_focused_window_rule(node: KdlNode) -> bool:
|
|
if node.name != "window-rule" or node.get_children("exclude"):
|
|
return False
|
|
matches = node.get_children("match")
|
|
if len(matches) != 1:
|
|
return False
|
|
match = matches[0]
|
|
return match.args == [] and match.props == {"is-focused": True}
|
|
|
|
|
|
def _global_window_rule(nodes: list[KdlNode]) -> KdlNode | None:
|
|
return next((n for n in reversed(nodes) if _is_global_window_rule(n)), None)
|
|
|
|
|
|
def _focused_window_rule(nodes: list[KdlNode]) -> KdlNode | None:
|
|
return next((n for n in reversed(nodes) if _is_focused_window_rule(n)), None)
|
|
|
|
|
|
def _blur_config_node(nodes: list[KdlNode]) -> KdlNode | None:
|
|
return next((n for n in reversed(nodes) if n.name == "blur"), None)
|
|
|
|
|
|
def _ensure_blur_config_node(nodes: list[KdlNode]) -> KdlNode:
|
|
blur = _blur_config_node(nodes)
|
|
if blur is None:
|
|
blur = KdlNode("blur")
|
|
blur.leading_trivia = "\n"
|
|
nodes.append(blur)
|
|
return blur
|
|
|
|
|
|
def _ensure_global_window_rule(nodes: list[KdlNode]) -> KdlNode:
|
|
rule = _global_window_rule(nodes)
|
|
if rule is None:
|
|
rule = KdlNode("window-rule")
|
|
rule.leading_trivia = "\n"
|
|
nodes.append(rule)
|
|
return rule
|
|
|
|
|
|
def _ensure_focused_window_rule(nodes: list[KdlNode]) -> KdlNode:
|
|
rule = _focused_window_rule(nodes)
|
|
if rule is None:
|
|
rule = KdlNode("window-rule")
|
|
rule.leading_trivia = "\n"
|
|
rule.children.append(KdlNode("match", props={"is-focused": True}))
|
|
nodes.append(rule)
|
|
return rule
|
|
|
|
|
|
def _background_effect(rule: KdlNode) -> KdlNode | None:
|
|
return rule.get_child("background-effect")
|
|
|
|
|
|
def _ensure_background_effect(rule: KdlNode) -> KdlNode:
|
|
effect = _background_effect(rule)
|
|
if effect is None:
|
|
effect = KdlNode("background-effect")
|
|
effect.leading_trivia = "\n"
|
|
rule.children.append(effect)
|
|
return effect
|
|
|
|
|
|
def _remove_rule_if_empty(nodes: list[KdlNode], rule: KdlNode) -> None:
|
|
if not rule.args and not rule.props and not rule.children and rule in nodes:
|
|
nodes.remove(rule)
|
|
|
|
|
|
def _remove_background_effect_if_empty(rule: KdlNode) -> None:
|
|
effect = _background_effect(rule)
|
|
if effect and not effect.args and not effect.props and not effect.children:
|
|
remove_child(rule, "background-effect")
|
|
|
|
|
|
def _compact_generated_spacing(node: KdlNode) -> None:
|
|
if not node.trailing_trivia or node.trailing_trivia.isspace():
|
|
node.trailing_trivia = "\n"
|
|
for child in node.children:
|
|
if not child.leading_trivia or child.leading_trivia.isspace():
|
|
child.leading_trivia = ""
|
|
if not child.trailing_trivia or child.trailing_trivia.isspace():
|
|
child.trailing_trivia = ""
|
|
if child.name == "background-effect":
|
|
_compact_generated_spacing(child)
|
|
|
|
|
|
def _sort_children_by_name(node: KdlNode, order: list[str]) -> None:
|
|
order_index = {name: index for index, name in enumerate(order)}
|
|
indexed_children = list(enumerate(node.children))
|
|
indexed_children.sort(
|
|
key=lambda item: (order_index.get(item[1].name, len(order)), item[0])
|
|
)
|
|
node.children = [child for _, child in indexed_children]
|
|
|
|
|
|
def _finalize_window_rule(rule: KdlNode) -> None:
|
|
effect = _background_effect(rule)
|
|
if effect is not None:
|
|
_sort_children_by_name(effect, _EFFECT_CHILD_ORDER)
|
|
_sort_children_by_name(rule, _RULE_CHILD_ORDER)
|
|
_compact_generated_spacing(rule)
|
|
|
|
|
|
def _finalize_blur_config(blur: KdlNode) -> None:
|
|
_sort_children_by_name(blur, _BLUR_CONFIG_CHILD_ORDER)
|
|
_compact_generated_spacing(blur)
|
|
|
|
|
|
def _rule_opacity(rule: KdlNode | None) -> float:
|
|
if rule is None:
|
|
return 1.0
|
|
value = rule.child_arg("opacity", 1.0)
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return 1.0
|
|
|
|
|
|
def get_global_draw_border_with_background(nodes: list[KdlNode]) -> bool:
|
|
rule = _global_window_rule(nodes)
|
|
if rule is None:
|
|
return True
|
|
return rule.child_arg("draw-border-with-background", True) is not False
|
|
|
|
|
|
def set_global_draw_border_with_background(nodes: list[KdlNode], enabled: bool) -> None:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
if enabled:
|
|
remove_child(rule, "draw-border-with-background")
|
|
else:
|
|
set_child_arg(rule, "draw-border-with-background", False)
|
|
_finalize_window_rule(rule)
|
|
_remove_rule_if_empty(nodes, rule)
|
|
|
|
|
|
def blur_effects_enabled(nodes: list[KdlNode]) -> bool:
|
|
blur = _blur_config_node(nodes)
|
|
return blur is None or blur.get_child("off") is None
|
|
|
|
|
|
def set_blur_effects_enabled(nodes: list[KdlNode], enabled: bool) -> None:
|
|
blur = _ensure_blur_config_node(nodes)
|
|
set_node_flag(blur, "off", not enabled)
|
|
_finalize_blur_config(blur)
|
|
_remove_rule_if_empty(nodes, blur)
|
|
if enabled:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
if _rule_opacity(rule) >= 1.0:
|
|
set_child_arg(rule, "opacity", 0.9)
|
|
_finalize_window_rule(rule)
|
|
else:
|
|
set_global_window_blur(nodes, False)
|
|
set_focused_window_blur(nodes, False)
|
|
|
|
|
|
def global_window_blur_enabled(nodes: list[KdlNode]) -> bool:
|
|
rule = _global_window_rule(nodes)
|
|
return _rule_blur_enabled(rule)
|
|
|
|
|
|
def focused_window_blur_enabled(nodes: list[KdlNode]) -> bool:
|
|
rule = _focused_window_rule(nodes)
|
|
return _rule_blur_enabled(rule)
|
|
|
|
|
|
def _rule_blur_enabled(rule: KdlNode | None) -> bool:
|
|
if rule is None:
|
|
return False
|
|
effect = _background_effect(rule)
|
|
if effect is None:
|
|
return False
|
|
blur = effect.get_child("blur")
|
|
return blur is not None and (not blur.args or blur.args[0] is True)
|
|
|
|
|
|
def set_global_window_blur(nodes: list[KdlNode], enabled: bool) -> None:
|
|
if enabled:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
if _rule_opacity(rule) >= 1.0:
|
|
set_child_arg(rule, "opacity", 0.9)
|
|
effect = _ensure_background_effect(rule)
|
|
set_child_arg(effect, "blur", True)
|
|
_finalize_window_rule(rule)
|
|
return
|
|
|
|
existing_rule = _global_window_rule(nodes)
|
|
if existing_rule is None:
|
|
return
|
|
existing_effect = _background_effect(existing_rule)
|
|
if existing_effect is not None:
|
|
remove_child(existing_effect, "blur")
|
|
_remove_background_effect_if_empty(existing_rule)
|
|
remove_child(existing_rule, "opacity")
|
|
_finalize_window_rule(existing_rule)
|
|
_remove_rule_if_empty(nodes, existing_rule)
|
|
|
|
|
|
def set_focused_window_blur(nodes: list[KdlNode], enabled: bool) -> None:
|
|
if enabled:
|
|
rule = _ensure_focused_window_rule(nodes)
|
|
|
|
effect = _ensure_background_effect(rule)
|
|
set_child_arg(effect, "blur", True)
|
|
_finalize_window_rule(rule)
|
|
return
|
|
|
|
existing_rule = _focused_window_rule(nodes)
|
|
if existing_rule is None:
|
|
return
|
|
existing_effect = _background_effect(existing_rule)
|
|
if existing_effect is not None:
|
|
remove_child(existing_effect, "blur")
|
|
_remove_background_effect_if_empty(existing_rule)
|
|
_finalize_window_rule(existing_rule)
|
|
if len(existing_rule.children) == 1 and existing_rule.get_child("match"):
|
|
nodes.remove(existing_rule)
|
|
|
|
|
|
def global_window_xray_enabled(nodes: list[KdlNode]) -> bool:
|
|
rule = _global_window_rule(nodes)
|
|
if rule is None:
|
|
return True
|
|
effect = _background_effect(rule)
|
|
if effect is None:
|
|
return True
|
|
xray = effect.get_child("xray")
|
|
return xray is None or not xray.args or xray.args[0] is True
|
|
|
|
|
|
def set_global_window_xray(nodes: list[KdlNode], enabled: bool) -> None:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
effect = _ensure_background_effect(rule)
|
|
set_child_arg(effect, "xray", enabled)
|
|
_finalize_window_rule(rule)
|
|
|
|
|
|
def get_global_window_opacity(nodes: list[KdlNode]) -> float:
|
|
return _rule_opacity(_global_window_rule(nodes))
|
|
|
|
|
|
def set_global_window_opacity(nodes: list[KdlNode], opacity: float) -> None:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
if opacity < 1.0:
|
|
set_child_arg(rule, "opacity", round(opacity, 2))
|
|
else:
|
|
remove_child(rule, "opacity")
|
|
_finalize_window_rule(rule)
|
|
_remove_rule_if_empty(nodes, rule)
|
|
|
|
|
|
def get_global_corner_radius(nodes: list[KdlNode]) -> int:
|
|
rule = _global_window_rule(nodes)
|
|
if rule is None:
|
|
return 0
|
|
value = rule.child_arg("geometry-corner-radius", 0)
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
|
|
|
|
def set_global_corner_radius(nodes: list[KdlNode], radius: int) -> None:
|
|
rule = _ensure_global_window_rule(nodes)
|
|
if radius > 0:
|
|
set_child_arg(rule, "geometry-corner-radius", radius)
|
|
set_child_arg(rule, "clip-to-geometry", True)
|
|
else:
|
|
remove_child(rule, "geometry-corner-radius")
|
|
remove_child(rule, "clip-to-geometry")
|
|
_finalize_window_rule(rule)
|
|
_remove_rule_if_empty(nodes, rule)
|