Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
AssignmentEntry.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
7
8#include <QColor>
9#include <QHash>
10#include <QList>
11#include <QString>
12#include <QUuid>
13#include <QVariantMap>
14#include <QtGlobal>
15
16#include <optional>
17
18namespace PhosphorZones {
19
33{
34 QString screenId; // Stable EDID-based identifier (or connector name fallback)
35 int virtualDesktop = 0; // 0 = all desktops
36 QString activity; // Empty = all activities
37
38 bool operator==(const LayoutAssignmentKey& other) const
39 {
40 return screenId == other.screenId && virtualDesktop == other.virtualDesktop && activity == other.activity;
41 }
42
56 static LayoutAssignmentKey fromGroupName(const QString& groupName, const QString& prefix)
57 {
59 if (!groupName.startsWith(prefix))
60 return result;
61 QString remainder = groupName.mid(prefix.size());
62 if (remainder.isEmpty())
63 return result;
64
65 constexpr QLatin1String kActivityTag(":Activity:");
66 constexpr QLatin1String kDesktopTag(":Desktop:");
67 int actIdx = remainder.indexOf(kActivityTag);
68 if (actIdx >= 0) {
69 const QString activity = remainder.mid(actIdx + kActivityTag.size());
70 if (!activity.isEmpty())
71 result.activity = activity;
72 remainder = remainder.left(actIdx);
73 }
74 int deskIdx = remainder.indexOf(kDesktopTag);
75 if (deskIdx >= 0) {
76 bool ok = false;
77 int desktop = remainder.mid(deskIdx + kDesktopTag.size()).toInt(&ok);
78 if (ok && desktop > 0)
79 result.virtualDesktop = desktop;
80 remainder = remainder.left(deskIdx);
81 }
82 result.screenId = remainder;
83 return result;
84 }
85};
86
87inline size_t qHash(const LayoutAssignmentKey& key, size_t seed = 0)
88{
89 seed = ::qHash(key.screenId, seed);
90 seed = ::qHash(key.virtualDesktop, seed);
91 seed = ::qHash(key.activity, seed);
92 return seed;
93}
94
132inline constexpr QLatin1String NoScrollingTemplate{"none"};
133
173inline constexpr QLatin1String NoSnappingLayout{"none"};
174
190inline constexpr QLatin1String NoTilingAlgorithm{"none"};
191
202{
212 enum Mode {
225 Scrolling = 2
226 };
228 QString snappingLayout; // UUID string of manual layout
229 QString tilingAlgorithm; // e.g. "dwindle", "wide", "tall"
253
254 QString activeLayoutId() const
255 {
256 // Scrolling has no layout entity, so the id is the bare sentinel
257 // "scrolling:". This is LOAD-BEARING: the assignment cascade's
258 // visitors reject entries with an empty activeLayoutId(), and a
259 // fresh mode-only Scrolling entry (empty preserved snappingLayout)
260 // would otherwise silently fail to assign. The gap / lock /
261 // overlay / tiling-param / scrolling-param resolvers stamp this
262 // sentinel into query.activeLayout, so `ActiveLayout Equals
263 // "scrolling:"` matches THERE (the assignment and context-default
264 // resolvers deliberately leave activeLayout unstamped — recursion).
265 // There is no per-layout identity to distinguish beyond the mode.
266 if (mode == Scrolling) {
268 }
269 if (mode == Autotile) {
270 // Autotile mode always produces a non-empty id so the cascade
271 // visitors in LayoutRegistry accept it (they reject on
272 // activeLayoutId().isEmpty()). For a mode-only entry (empty
273 // tilingAlgorithm — what the KCM writes for "autotile, use
274 // default algorithm"), makeAutotileId returns the bare
275 // prefix @c "autotile:"; downstream callers use
276 // @ref PhosphorLayout::LayoutId::isAutotile to detect mode
277 // and @ref PhosphorLayout::LayoutId::extractAlgorithmId to
278 // get the algorithm (empty = engine default).
280 }
281 return snappingLayout;
282 }
293 bool isValid() const
294 {
295 return !snappingLayout.isEmpty() || !tilingAlgorithm.isEmpty();
296 }
297 bool operator==(const AssignmentEntry& other) const
298 {
299 return mode == other.mode && snappingLayout == other.snappingLayout && tilingAlgorithm == other.tilingAlgorithm
301 }
302
308 static AssignmentEntry fromLayoutId(const QString& layoutId, const AssignmentEntry& existing)
309 {
310 AssignmentEntry entry = existing;
312 entry.mode = Autotile;
314 } else if (PhosphorLayout::LayoutId::isScrolling(layoutId)) {
315 // The "scrolling:" sentinel carries no layout entity — flip the
316 // mode and preserve all three layout fields (the lossless-toggle
317 // contract), so a get→set round-trip cannot degrade a Scrolling
318 // assignment into Snapping-pointing-at-a-bogus-id.
319 entry.mode = Scrolling;
320 } else {
321 entry.mode = Snapping;
322 // Normalize a UUID-shaped id to its canonical braced spelling at
323 // this ONE classification choke point, so every caller (the D-Bus
324 // setAssignmentEntry and the four setAll*Assignments verbs, the
325 // batch rebuilds, the controllers) stores one spelling. A braceless
326 // or upper-case bus-supplied uuid stored verbatim defeats the
327 // exact-string compare that purgeLayoutIdFromAssignments does on
328 // layout delete, and the byte-wise action compare in
329 // upsertAssignmentRule's no-op guard. Anything that is not a UUID
330 // passes through untouched — the snapping slot is not required to
331 // hold one, and @ref NoSnappingLayout DEPENDS on this arm staying
332 // verbatim: parsing the reserved word yields a null QUuid, and
333 // storing that null's toString would silently turn "explicitly
334 // none" into a dangling id.
335 const QUuid parsed = QUuid::fromString(layoutId);
336 entry.snappingLayout = parsed.isNull() ? layoutId : parsed.toString();
337 }
338 return entry;
339 }
344 static AssignmentEntry fromLayoutId(const QString& layoutId)
345 {
346 return fromLayoutId(layoutId, AssignmentEntry{});
347 }
348};
349
362{
363 std::optional<int> innerGap;
364 std::optional<int> outerGap;
365 std::optional<bool> usePerSideOuterGap;
366 std::optional<int> outerGapTop;
367 std::optional<int> outerGapBottom;
368 std::optional<int> outerGapLeft;
369 std::optional<int> outerGapRight;
370
371 bool isEmpty() const
372 {
374 && !outerGapRight;
375 }
376};
377
400{
401 std::optional<QString> shaderId;
402 QVariantMap shaderParams;
403 std::optional<int> style;
404 std::optional<QColor> highlightColor;
405 std::optional<QColor> inactiveColor;
406 std::optional<QColor> borderColor;
407 std::optional<double> activeOpacity;
408 std::optional<double> inactiveOpacity;
409 std::optional<int> borderWidth;
410 std::optional<int> borderRadius;
411 std::optional<bool> showZoneNumbers;
412
413 bool isEmpty() const
414 {
415 // shaderParams is only ever populated alongside shaderId, but check it too so
416 // isEmpty() stays honest if a future writer sets the map without the gate.
417 return !shaderId && shaderParams.isEmpty() && !style && !highlightColor && !inactiveColor && !borderColor
419 }
420};
421
434{
435 std::optional<int> maxWindows;
436 std::optional<double> splitRatio;
437 std::optional<int> masterCount;
441 std::optional<int> insertPosition;
443 std::optional<int> overflowBehavior;
446 std::optional<int> dragBehavior;
452 QVariantMap algorithmParams;
453
454 bool isEmpty() const
455 {
456 // algorithmParams is only ever populated alongside algorithmParamTarget, but
457 // check it too so isEmpty() stays honest if a future writer sets it without the
458 // target.
460 && algorithmParamTarget.isEmpty() && algorithmParams.isEmpty();
461 }
462};
463
481{
484 std::optional<double> defaultColumnWidth;
488 std::optional<int> centerFocusedColumn;
490 std::optional<int> defaultColumnDisplay;
493 std::optional<int> insertPosition;
496 std::optional<double> defaultWindowHeight;
497
505 std::optional<bool> alwaysCenterSingleColumn;
506 std::optional<bool> respectMinimumSize;
507 std::optional<bool> cropStraddlers;
508 std::optional<bool> focusNewWindows;
509 std::optional<bool> smartGaps;
513 std::optional<bool> focusFollowsMouse;
519 std::optional<double> focusFollowsMouseMaxScroll;
523 std::optional<int> stickyWindowHandling;
531 std::optional<int> stripAxis;
532
541 std::optional<bool> tabIndicatorEnabled;
542 std::optional<bool> tabIndicatorHideWhenSingleTab;
543 std::optional<bool> tabIndicatorPlaceWithinColumn;
544 std::optional<int> tabIndicatorGap;
545 std::optional<int> tabIndicatorWidth;
546 std::optional<double> tabIndicatorLength;
547 std::optional<int> tabIndicatorPosition;
548 std::optional<int> tabIndicatorStyle;
549 std::optional<int> tabIndicatorGapsBetweenTabs;
550 std::optional<int> tabIndicatorCornerRadius;
551 std::optional<QString> tabIndicatorActiveColor;
552 std::optional<QString> tabIndicatorInactiveColor;
553 std::optional<QString> tabIndicatorUrgentColor;
559 std::optional<QString> tabIndicatorFontFamily;
560 std::optional<int> tabIndicatorFontWeight;
561 std::optional<bool> tabIndicatorFontItalic;
562 std::optional<bool> tabIndicatorFontUnderline;
563 std::optional<bool> tabIndicatorFontStrikeout;
564
570 std::optional<bool> dropIndicatorEnabled;
571 std::optional<QString> dropIndicatorColor;
572 std::optional<QString> dropIndicatorBorderColor;
573 std::optional<double> dropIndicatorOpacity;
574 std::optional<int> dropIndicatorBorderWidth;
575 std::optional<int> dropIndicatorBorderRadius;
576
577 // The three block-level aggregates below exist for isEmpty() — their
578 // ONLY caller. Earlier docs claimed the daemon consulted them to skip
579 // whole override blocks; it never did (it tests each optional it
580 // consumes directly), so do not add such a gate on the strength of a
581 // predicate existing here.
582
589
599
608
615};
616
628{
629 switch (mode) {
631 return QStringLiteral("snapping");
633 return QStringLiteral("autotile");
635 return QStringLiteral("scrolling");
636 }
637 // Switch is exhaustive over `Mode`. A `static_cast<Mode>(99)` reaching
638 // this line means either (a) a future Mode enum value was added without
639 // a case here — the real guard against that is Qt's -Wswitch diagnostic
640 // at the missing-case site (NOTE: this project's CMake does NOT
641 // promote it to -Werror, so it's a warning at build time, not an
642 // error), or (b) callers fabricated an out-of-range value via cast.
643 // `Q_UNREACHABLE_RETURN` expands to `[[unreachable]] + return`
644 // on modern compilers, so the sentinel below carries through release
645 // builds even when the optimizer assumes the function never reaches
646 // here.
647 //
648 // The sentinel `"invalid"` is rejected by the `DisableEngine`
649 // descriptor's closed-vocabulary validator
650 // (`engineModeOptions().contains(...)`), so a malformed disable rule
651 // fails load loudly. NOTE: `SetEngineMode`'s validator only checks
652 // `hasNonEmptyString` (open-vocabulary by design — see its descriptor
653 // validator in `libs/phosphor-rules/src/ruleaction_builtins_engine.cpp`), so a
654 // malformed assignment rule survives load but is silently coerced
655 // back to Snapping at consumption via
656 // `entryFromRuleMatchActions → modeFromWireString → nullopt`. The
657 // sentinel makes the corruption visible to operators inspecting
658 // rules.json by eye, but is not a load-time gate for the
659 // assignment path.
660 Q_UNREACHABLE_RETURN(QStringLiteral("invalid"));
661}
662
671inline std::optional<AssignmentEntry::Mode> modeFromWireString(const QString& wire)
672{
673 if (wire == QLatin1String("snapping")) {
675 }
676 if (wire == QLatin1String("autotile")) {
678 }
679 if (wire == QLatin1String("scrolling")) {
681 }
682 return std::nullopt;
683}
684
695inline QList<AssignmentEntry::Mode> allModes()
696{
698}
699
700} // namespace PhosphorZones
constexpr QLatin1String ScrollingId
Scrolling has no layout entity, so its whole id is the bare sentinel — it exists so a mode-only Scrol...
Definition LayoutId.h:37
QString makeAutotileId(const QString &algorithmId)
Definition LayoutId.h:76
bool isScrolling(const QString &id)
Definition LayoutId.h:44
bool isAutotile(const QString &id)
Definition LayoutId.h:39
QString extractAlgorithmId(const QString &id)
Extract the algorithm id portion from an autotile preview id.
Definition LayoutId.h:64
Definition IWindowTrackingService.h:23
size_t qHash(const LayoutAssignmentKey &key, size_t seed=0)
Definition AssignmentEntry.h:87
constexpr QLatin1String NoTilingAlgorithm
Reserved value of AssignmentEntry::tilingAlgorithm meaning "this context explicitly uses NO tiling al...
Definition AssignmentEntry.h:190
std::optional< AssignmentEntry::Mode > modeFromWireString(const QString &wire)
Inverse of modeToWireString.
Definition AssignmentEntry.h:671
QString modeToWireString(AssignmentEntry::Mode mode)
Canonical wire-string for an AssignmentEntry::Mode.
Definition AssignmentEntry.h:627
constexpr QLatin1String NoScrollingTemplate
Reserved value of AssignmentEntry::scrollingTemplateLayout meaning "this context explicitly uses NO s...
Definition AssignmentEntry.h:132
constexpr QLatin1String NoSnappingLayout
Reserved value of AssignmentEntry::snappingLayout meaning "this context explicitly uses NO snapping l...
Definition AssignmentEntry.h:173
QList< AssignmentEntry::Mode > allModes()
Iteration order for every AssignmentEntry::Mode value.
Definition AssignmentEntry.h:695
Explicit per-context assignment entry storing every mode's payload.
Definition AssignmentEntry.h:202
QString snappingLayout
Definition AssignmentEntry.h:228
static AssignmentEntry fromLayoutId(const QString &layoutId)
Create a fresh AssignmentEntry from a layoutId string.
Definition AssignmentEntry.h:344
static AssignmentEntry fromLayoutId(const QString &layoutId, const AssignmentEntry &existing)
Update an existing AssignmentEntry from a layoutId, preserving the "other" field.
Definition AssignmentEntry.h:308
QString activeLayoutId() const
Definition AssignmentEntry.h:254
Mode
Per-context engine selection.
Definition AssignmentEntry.h:212
@ Scrolling
The niri-style scrolling engine (PhosphorScrollEngine): windows form columns on an endless strip per ...
Definition AssignmentEntry.h:225
@ Snapping
Definition AssignmentEntry.h:213
@ Autotile
Definition AssignmentEntry.h:214
QString scrollingTemplateLayout
Id of the native scrolling template (ScrollingTemplate) a Scrolling context uses for its seed bluepri...
Definition AssignmentEntry.h:252
bool operator==(const AssignmentEntry &other) const
Definition AssignmentEntry.h:297
bool isValid() const
True when the entry carries a layout/algorithm PAYLOAD — including the reserved opt-out word,...
Definition AssignmentEntry.h:293
QString tilingAlgorithm
Definition AssignmentEntry.h:229
Mode mode
Definition AssignmentEntry.h:227
Per-context gap override resolved from rules.
Definition AssignmentEntry.h:362
std::optional< int > outerGapLeft
Definition AssignmentEntry.h:368
std::optional< int > outerGapBottom
Definition AssignmentEntry.h:367
std::optional< int > outerGapTop
Definition AssignmentEntry.h:366
bool isEmpty() const
Definition AssignmentEntry.h:371
std::optional< bool > usePerSideOuterGap
Definition AssignmentEntry.h:365
std::optional< int > outerGap
Definition AssignmentEntry.h:364
std::optional< int > innerGap
Definition AssignmentEntry.h:363
std::optional< int > outerGapRight
Definition AssignmentEntry.h:369
Per-context overlay-property overrides resolved from window-rule actions.
Definition AssignmentEntry.h:400
std::optional< int > style
Definition AssignmentEntry.h:403
std::optional< QColor > inactiveColor
Definition AssignmentEntry.h:405
std::optional< int > borderRadius
Definition AssignmentEntry.h:410
std::optional< int > borderWidth
Definition AssignmentEntry.h:409
QVariantMap shaderParams
Definition AssignmentEntry.h:402
std::optional< double > inactiveOpacity
Definition AssignmentEntry.h:408
std::optional< QString > shaderId
Definition AssignmentEntry.h:401
bool isEmpty() const
Definition AssignmentEntry.h:413
std::optional< QColor > highlightColor
Definition AssignmentEntry.h:404
std::optional< QColor > borderColor
Definition AssignmentEntry.h:406
std::optional< double > activeOpacity
Definition AssignmentEntry.h:407
std::optional< bool > showZoneNumbers
Definition AssignmentEntry.h:411
Per-context scrolling parameter overrides resolved from context rules.
Definition AssignmentEntry.h:481
std::optional< bool > tabIndicatorFontUnderline
Definition AssignmentEntry.h:562
std::optional< QString > tabIndicatorInactiveColor
Definition AssignmentEntry.h:552
std::optional< bool > dropIndicatorEnabled
The drop indicator's overrides.
Definition AssignmentEntry.h:570
std::optional< bool > smartGaps
Definition AssignmentEntry.h:509
std::optional< bool > tabIndicatorPlaceWithinColumn
Definition AssignmentEntry.h:543
std::optional< int > centerFocusedColumn
When the viewport re-centres on the focused column (0 = never, 1 = always, 2 = on overflow); the reso...
Definition AssignmentEntry.h:488
std::optional< QString > tabIndicatorActiveColor
Definition AssignmentEntry.h:551
std::optional< int > dropIndicatorBorderRadius
px; 0 is square, no sentinel
Definition AssignmentEntry.h:575
std::optional< QString > tabIndicatorFontFamily
The label font, all PAINT.
Definition AssignmentEntry.h:559
std::optional< QString > dropIndicatorBorderColor
Definition AssignmentEntry.h:572
std::optional< int > tabIndicatorPosition
TabIndicatorPosition ints, left 0 … bottom 3.
Definition AssignmentEntry.h:547
bool hasTabIndicatorOverrides() const
True when at least one of the eighteen tab-indicator slots resolved.
Definition AssignmentEntry.h:591
std::optional< int > tabIndicatorFontWeight
CSS scale, 100..900; 400 regular, 700 bold.
Definition AssignmentEntry.h:560
std::optional< int > tabIndicatorStyle
0 = title chips, 1 = segment bar
Definition AssignmentEntry.h:548
std::optional< bool > focusNewWindows
Definition AssignmentEntry.h:508
std::optional< int > tabIndicatorGap
px; NEGATIVE draws the indicator over the window
Definition AssignmentEntry.h:544
std::optional< double > defaultColumnWidth
Width a newly-opened column takes, as a fraction of the work area (0.05-1.0).
Definition AssignmentEntry.h:484
std::optional< int > dropIndicatorBorderWidth
px; 0 is a fill with no edge
Definition AssignmentEntry.h:574
std::optional< int > insertPosition
Where a fresh-opened window's column enters the strip (ScrollInsertPosition ints, right-of-active 0 …...
Definition AssignmentEntry.h:493
std::optional< bool > tabIndicatorHideWhenSingleTab
Definition AssignmentEntry.h:542
std::optional< bool > tabIndicatorEnabled
The tab indicator's overrides, niri's tab-indicator layout block.
Definition AssignmentEntry.h:541
std::optional< int > tabIndicatorGapsBetweenTabs
px
Definition AssignmentEntry.h:549
std::optional< bool > focusFollowsMouse
EFFECT-consumed, unlike its neighbours: the daemon collects the resolved per-screen verdict into a se...
Definition AssignmentEntry.h:513
std::optional< bool > tabIndicatorFontStrikeout
Definition AssignmentEntry.h:563
std::optional< QString > tabIndicatorUrgentColor
Definition AssignmentEntry.h:553
std::optional< bool > respectMinimumSize
Definition AssignmentEntry.h:506
std::optional< int > stickyWindowHandling
StickyWindowHandling ints (treatAsNormal 0 / restoreOnly 1 / ignoreAll 2); the resolver maps the wire...
Definition AssignmentEntry.h:523
std::optional< bool > tabIndicatorFontItalic
Definition AssignmentEntry.h:561
bool hasDropIndicatorOverrides() const
True when at least one drop-indicator slot resolved.
Definition AssignmentEntry.h:584
std::optional< int > defaultColumnDisplay
How a newly-opened column lays its windows out (0 = normal, 1 = tabbed).
Definition AssignmentEntry.h:490
std::optional< int > tabIndicatorCornerRadius
px; -1 is the "fully rounded" sentinel
Definition AssignmentEntry.h:550
std::optional< double > focusFollowsMouseMaxScroll
The cap on the toggle above, as a FRACTION of the viewport's extent along the strip (niri's max-scrol...
Definition AssignmentEntry.h:519
bool isEmpty() const
Definition AssignmentEntry.h:609
std::optional< bool > alwaysCenterSingleColumn
The scrolling BEHAVIOUR toggles, filled by the SetScroll* actions of the same names.
Definition AssignmentEntry.h:505
std::optional< double > tabIndicatorLength
fraction of the column extent
Definition AssignmentEntry.h:546
std::optional< int > stripAxis
Which way the strip runs, in the Scrolling.StripAxis config INTENT space (auto 0 / horizontal 1 / ver...
Definition AssignmentEntry.h:531
std::optional< double > defaultWindowHeight
Height a newly-opened window takes, as a fraction of the work-area height (0.05-1....
Definition AssignmentEntry.h:496
std::optional< double > dropIndicatorOpacity
fill only; the border is always opaque
Definition AssignmentEntry.h:573
std::optional< int > tabIndicatorWidth
px thickness
Definition AssignmentEntry.h:545
std::optional< bool > cropStraddlers
Definition AssignmentEntry.h:507
std::optional< QString > dropIndicatorColor
Definition AssignmentEntry.h:571
bool hasBehaviourOverrides() const
True when at least one of the seven behaviour toggles resolved, or the scroll cap did.
Definition AssignmentEntry.h:603
Per-context autotile parameter overrides resolved from context rules.
Definition AssignmentEntry.h:434
std::optional< int > insertPosition
The AutotileInsertPosition int (0 = End, 1 = AfterFocused, 2 = AsMaster); the resolver maps the wire ...
Definition AssignmentEntry.h:441
std::optional< int > dragBehavior
The AutotileDragBehavior int (0 = Float, 1 = Reorder).
Definition AssignmentEntry.h:446
std::optional< int > masterCount
Definition AssignmentEntry.h:437
std::optional< int > overflowBehavior
The AutotileOverflowBehavior int (0 = Float, 1 = Unlimited).
Definition AssignmentEntry.h:443
QString algorithmParamTarget
A SetAlgorithmParam override: the target algorithm id and the custom-param values to layer over that ...
Definition AssignmentEntry.h:451
std::optional< int > maxWindows
Definition AssignmentEntry.h:435
QVariantMap algorithmParams
Definition AssignmentEntry.h:452
std::optional< double > splitRatio
Definition AssignmentEntry.h:436
bool isEmpty() const
Definition AssignmentEntry.h:454
Key for layout assignment (screen + desktop + activity)
Definition AssignmentEntry.h:33
static LayoutAssignmentKey fromGroupName(const QString &groupName, const QString &prefix)
Parse a "<prefix><screenId>[:Desktop:N][:Activity:uuid]" group name.
Definition AssignmentEntry.h:56
bool operator==(const LayoutAssignmentKey &other) const
Definition AssignmentEntry.h:38
QString screenId
Definition AssignmentEntry.h:34
int virtualDesktop
Definition AssignmentEntry.h:35
QString activity
Definition AssignmentEntry.h:36