Classification & Learned Rules
flow classifies every new window as tiling, floating, or ignored. This
chapter covers why the default is float, how flow learns from the user’s
explicit set-window decisions, and where that learned state lives on disk.
For the rule-matching algorithm itself — the Win32 pre-filters, the per-layer
first-match-wins evaluation, and the regex/field semantics — see
Window Registry.
The Whitelist Model: Float by Default
Most tiling window managers take a blacklist approach: every window tiles
unless the user adds a rule to float it. flow inverts this. Every window
floats unless the user promotes it to tiling — either with an explicit rule
in flow-rules.toml or by running flow dispatch set-window tile on it once.
This whitelist model is encoded in a single value: the default_action field
of WindowRulesConfig defaults to Float (see
src/config/types.rs). When no rule at any layer
matches a window, the pipeline returns Float rather than Tile. The bundled
default-flow-rules.toml carries default_action = "float" to match.
The rationale: on a scrolling-canvas tiler like flow, most application windows (notably transients, dialogs, and small utilities) look wrong in a tile column. Tiling is most useful for a small set of “main” apps the user actively works in — and the user is the best judge of which apps those are.
The Four-Layer Priority Chain
Every classification consults four layers in priority order. The first layer to match wins; layers below it are not consulted:
flowchart TB
W["New window<br/>(exe, title, class)"] --> L1{"User rules<br/>(flow-rules.toml)?"}
L1 -- match --> R1["use user rule"]
L1 -- no match --> L2{"Learned rules<br/>(history-flow-rules.toml)?"}
L2 -- match --> R2["use learned rule"]
L2 -- no match --> L3{"Default rules<br/>(embedded at compile time)?"}
L3 -- match --> R3["use default rule"]
L3 -- no match --> L4["default_action<br/>= Float"]
R1 --> OUT["WindowState"]
R2 --> OUT
R3 --> OUT
L4 --> OUT
The four layers, from highest to lowest priority:
- User rules — hand-written in
flow-rules.toml. Always win. The escape hatch for “I want this app to behave this way regardless of what flow learned.” - Learned rules — machine-written to
history-flow-rules.toml. See Learned Rules below. - Default rules — embedded into the binary at compile time from
default-flow-rules.toml. Catch well-known system windows (taskbar, Chromium helper windows, dialogs). default_action— the unconditional fallback, nowFloat.
User rules outrank learned rules deliberately: if flow’s learned behavior
conflicts with the user’s intent, the user’s flow-rules.toml entry wins and
the learned rule is never consulted for that app.
Learned Rules
Learned rules are flow’s memory of the user’s explicit float/tile decisions.
When the user runs flow dispatch set-window float or flow dispatch set-window tile on a window, flow records the decision keyed on that app’s identity. The
next time a window of the same app appears, it is classified automatically —
no rule writing required.
What Triggers a Recording
Only actual transitions are recorded. A set-window call that resolves to a
no-op (the window was already in the requested mode) records nothing. This
keeps the history file free of redundant entries and makes repeated commands
idempotent.
The recording happens inside dispatch_set_window, after the transition
succeeds:
sequenceDiagram
participant CLI as flow CLI
participant D as dispatch_set_window
participant H as HistoryStore
participant R as WindowRegistry
participant FS as history-flow-rules.toml
CLI->>D: set-window float
D->>D: resolve action (MakeFloating / MakeTiling / NoOp)
D->>R: execute transition
R-->>D: Ok
alt action != NoOp
D->>H: record(action, exe, class)
H-->>D: changed? (dedup-and-update)
alt changed
D->>FS: save (atomic: temp + rename)
D->>R: set_learned_rules(refresh pipeline)
end
end
D-->>CLI: Ok
The pipeline refresh (set_learned_rules) is what makes the new decision take
effect immediately — the very next window of that app is classified using the
updated learned layer, with no daemon restart.
App Identity: exe + class
The identity key for a learned rule is the app’s executable name plus its
Win32 window class name (e.g. chrome.exe + Chrome_WidgetWin_1). Both
fields are used when the class is non-empty; when the class is empty, the key
falls back to exe alone.
Title is deliberately not part of the key. A window’s title changes constantly (the document name, the current tab, the cursor position), so keying on it would fragment one app into dozens of “different” apps. The exe + class pair is stable for the lifetime of an application version.
Dedup-and-Update, Not Append
When the user toggles an app’s mode repeatedly — float it, then tile it, then
float it again — flow does not append a new rule each time. Instead, the
existing learned rule for that exe + class is updated in place. This keeps
the history file bounded and makes the most recent decision authoritative.
If append were used instead, first-match-wins evaluation would keep returning the first recorded (stale) decision forever, and the file would grow without bound. Dedup-and-update avoids both problems.
The History File
Learned rules persist to history-flow-rules.toml in the config directory
(default %USERPROFILE%\.config\flow\, overridable via FLOW_CONFIG_DIR). The
file uses the same schema as flow-rules.toml — a default_action field and a
[[rules]] array — so it is human-readable and human-editable. flow writes it
atomically (write to .toml.tmp, then rename) so a crash mid-write cannot
corrupt the existing file.
The default_action field in the history file is ignored at load time; only
the rules array is read. (The field exists only because the schema is shared
with flow-rules.toml.)
Clearing or Overriding Learned State
To forget everything flow has learned:
-
Delete
history-flow-rules.toml— flow recreates it (empty) on the next recorded transition. Until then, classification falls through to the default rules anddefault_action = Float. -
Delete a single app’s entry — open the file in a text editor and remove the matching
[[rules]]block. flow will not re-add it until the user runsset-windowon that app again. -
Override a learned rule permanently — add an explicit rule for the app in
flow-rules.toml. User rules outrank learned rules, so the user’s choice always wins regardless of what the history file says.
The
ForgetApp/ForgetAllAppsIPC commands (declared insrc/ipc/message.rs) are intended as a programmatic way to clear learned state without touching the file by hand. They are not yet implemented.
Where the Code Lives
| Concern | Location |
|---|---|
| History store (load, record, dedup, save) | src/config/history.rs |
| History file path resolution | history_rules_path[_in] in src/config/dirs.rs |
default_action = Float default | Default impl for WindowRulesConfig in src/config/types.rs |
| Four-layer pipeline evaluation | ClassificationPipeline::classify in src/registry/classification.rs |
| Runtime pipeline refresh | set_learned_rules on ClassificationPipeline and WindowRegistry |
Capture on set-window transition | record_learned_transition in src/daemon/dispatch.rs |
| Daemon-owned history store | history field on FlowWM, loaded in new() |
Cross-References
- Window Registry — the full classification algorithm: Win32 pre-filters, the rule-pipeline flow diagram, and per-field match semantics.
- Config & Persistence — the
flow-rules.tomlformat for user-defined rules and the “code is the source of truth” config philosophy. - Floating Space — where float-classified windows live and how tile↔float transitions are animated.