Flicker.Trigger (Flicker v0.1.0)

Copy Markdown View Source

The facet-entry trigger character (Spec 024) — parsing and validating the facet_trigger attr, and answering "does this token open the facet menu, and for which facets".

Without a trigger, every bare word the user types is classified {:key, prefix} and offers facet-key suggestions. That is right for a dedicated filter bar and wrong for a search box whose main job is free text. A trigger gates discovery on an explicit gesture: @ opens the facet menu, and nothing else does.

The invariant

A trigger is input sugar and never grammar. Flicker.Query.parse/2 has no clause for it, no token contains it, no pill displays it, and no URL carries it. @stat completes to status: — the trigger disappears because choosing a suggestion replaces the whole token, not because anything strips it.

This is the same separation ADR-013 draws between canonical tokens and localised display, applied to input affordances: the affordance is configurable precisely because nothing persists it.

Gating discovery, not recognition

A trigger stops Flicker offering facet keys unprompted. It does not stop it understanding one that is already there: status:active typed out in full, or pasted, or restored from a URL, still classifies as a facet value against the whole registry. If the trigger gated recognition too, a shared search URL would silently stop filtering.

Configuration

facet_trigger="@"                              # one trigger, every facet
facet_trigger={%{"@" => [:worker], "#" => [:tag]}}  # scoped, Slack-style

A trigger must be a single grapheme, must not be whitespace, and must not be a character that can legally start a facet key (a letter, digit, _ or ?) — s as a trigger would be indistinguishable from the first letter of status. : is allowed: it is the operator, but the operator only ever appears after a key run, so a token-initial : is unambiguous (:stat is a trigger, status: is not).

Summary

Types

What a host passes as facet_trigger.

t()

Trigger grapheme to the facets it offers — :all for the whole registry, or an explicit list of facet keys.

Functions

The configured graphemes, sorted — the list the discoverability hint renders.

Normalises a facet_trigger attr into a t/0, or nil for "no trigger configured" (today's always-on facet suggestions).

The facets grapheme offers, or nil if it isn't a configured trigger.

Types

config()

@type config() :: nil | String.t() | map() | keyword()

What a host passes as facet_trigger.

t()

@type t() :: %{required(String.t()) => :all | [atom()]}

Trigger grapheme to the facets it offers — :all for the whole registry, or an explicit list of facet keys.

Functions

graphemes(trigger)

@spec graphemes(t() | nil) :: [String.t()]

The configured graphemes, sorted — the list the discoverability hint renders.

Sorted rather than in configuration order so the hint is stable across renders regardless of how the host's map happens to enumerate.

Examples

iex> Flicker.Trigger.graphemes(%{"@" => :all, "#" => [:tag]})
["#", "@"]

iex> Flicker.Trigger.graphemes(nil)
[]

parse!(grapheme)

@spec parse!(config()) :: t() | nil

Normalises a facet_trigger attr into a t/0, or nil for "no trigger configured" (today's always-on facet suggestions).

Raises ArgumentError on anything unusable rather than degrading, because a mistyped trigger silently disabling every facet is a far worse failure than a crash in dev.

Examples

iex> Flicker.Trigger.parse!(nil)
nil

iex> Flicker.Trigger.parse!("@")
%{"@" => :all}

iex> Flicker.Trigger.parse!(%{"@" => [:worker], "#" => [:tag]})
%{"#" => [:tag], "@" => [:worker]}

iex> Flicker.Trigger.parse!(%{})
nil

iex> Flicker.Trigger.parse!("@@")
** (ArgumentError) facet_trigger must be a single character, got: "@@"

iex> Flicker.Trigger.parse!("s")
** (ArgumentError) facet_trigger cannot be a character that starts a facet key, got: "s"

scope(trigger, grapheme, facets)

@spec scope(t() | nil, String.t(), [Flicker.Facet.t()]) :: [Flicker.Facet.t()] | nil

The facets grapheme offers, or nil if it isn't a configured trigger.

nil is the caller's signal to treat the token as ordinary text rather than as facet entry — it is deliberately distinct from [], which means "a configured trigger whose facets are all filtered out".

Examples

iex> facets = [Flicker.Facet.new(key: :worker), Flicker.Facet.new(key: :tag)]
...> Flicker.Trigger.scope(%{"@" => [:worker]}, "@", facets) |> Enum.map(& &1.key)
[:worker]

iex> facets = [Flicker.Facet.new(key: :worker)]
...> Flicker.Trigger.scope(%{"@" => :all}, "#", facets)
nil