An expression is a value that happens to be a predicate. It drops into a stream unchanged, and — unlike a lambda — it can also be printed, stored, or translated into another system's filter language.
server.sessions().get(0).newWindow("editor");
List<Window> editors = server.windows().stream()
.filter(Window_.name().startsWith("edit"))
.toList();
Window only = Selections.exactlyOne(editors);
editors.size(); // → 1
only.name(); // → editorSession_, Window_, Pane_ and Client_ expose one handle per field, and
each handle offers only the operators its type supports. Asking a flag to start
with a string is a compile error, not a runtime cast failure.
Field ids are tmux's own format names — pane_current_command, window_name —
which is what keeps an expression meaningful to something that is not this
library.
and, or and negate compose expressions. Relations quantify:
var busy = Window_.panes().any(Pane_.command().startsWith("nv"));any, all and none cross a to-many relation; is crosses a to-one. all
over an empty relation is true — a session with no windows does not fail "all
windows are zoomed".
var busy = Window_.panes().any(Pane_.command().startsWith("nv"));
busy.describe(); // → panes any (pane_current_command starts-with nv)This is the half a lambda cannot do, and the reason the AST is a sealed tree of records rather than a captured function.
Selections.exactlyOne raises distinct exceptions for none and for several,
because those are different bugs in a caller's code. Selections.oneOrEmpty
returns an Optional but still raises on several. findFirst stays on Stream
where it already is.
An expression evaluates locally over a capture you already hold. Filtering issues no commands, so a stream pipeline costs nothing and cannot observe a half-changed server.
Expressions retain enough structure for a future compiler to lower them to tmux's
own -f predicate, but no release does that today, and no such compiler would
change what snapshot filtering means.
The optional libtmux-jackson module gives an expression a versioned wire form.
This snippet is exercised by FilterJsonTest rather than ExamplesTest, since
the core suite does not depend on Jackson:
String json = FilterJson.writeString(Pane_.command().startsWith("nv"), "pane");
FilterExpr<Pane> restored = FilterJson.readString(json, LibTmuxModels.pane());
restored.describe(); // → pane_current_command starts-with nvOnly expressions built from a metamodel can be written. A field built from a lambda has a caller-chosen name and an accessor nobody else can resolve, so it has no wire identity, and refusing it is what makes this a format rather than a hope.
Reading is validated against a model: a document claiming pane cannot be read
as a FilterExpr<Window>. Unknown schema versions, models, fields, relations,
operators and node shapes all fail closed.
Field and operator identifiers are tmux's own format names — pane_current_command,
not anything Java calls a field. So the document means the same thing to every
port of libtmux, and to a caller that is not a Java program at all.
libtmux-mcp is the worked example. Its tmux_list_panes tool takes an optional
filter, which is one of these documents:
{"schema": "libtmux.filter/1", "model": "pane",
"expr": {"node": "compare", "field": "pane_current_command",
"op": "starts_with", "value": "nvim"}}A model cannot write Java, so this is the only way it can say what it wants narrowed. What it gets back costs the same one capture the unfiltered listing would have, because the filter runs over what that capture returned.
A CLI flag, a config file or a stored query carries the field and the operator as
untrusted text. LegacyFilters is the one supported way in, so the rest of the
library never has to accept that shape. The key is field__operator; a bare
field name means equality.
var catalog = LegacyFilters.FieldCatalog.<Pane>builder()
.add("index", Pane_.index())
.add("active", Pane_.active())
.build();
FilterExpr<Pane> here = catalog.parse(Map.of("index__lt", "1"));
here.describe(); // → (pane_index < 1)
server.panes().stream().filter(here).toList().size(); // → 1The catalog decides which identifiers a caller may name, so an unknown field is
refused rather than guessed. Note what describe prints: the catalog key is the
alias you chose to expose, but the expression underneath carries tmux's own
pane_index, which is what keeps it meaningful to a port that is not this one.
Everything this refuses is a compile error in the typed form — a text operator on a number field, an ordering operator on text, a value of the wrong type. That is the trade made explicit: one place where a wrong name becomes a runtime failure, and the type system everywhere else.
Prefer accepting the entities and letting the caller filter:
public List<PaneSummary> describe(Collection<Pane> panes) { … }rather than accepting the expression and filtering inside. A method taking a
FilterExpr reads as though tmux did the selecting, and it does not. Reserve
FilterExpr parameters for code that inspects or translates an expression —
serializing it, or lowering it — which is what FilterJson does.