Skip to content

Latest commit

 

History

History
152 lines (109 loc) · 5.71 KB

File metadata and controls

152 lines (109 loc) · 5.71 KB

Filtering

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();                         // → editor

Typed fields

Session_, 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.

Composition and relations

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".

Saying what it is

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.

Cardinality

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.

Filtering never asks tmux

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.

Writing an expression down

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 nv

Only 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.

Who the wire form is actually for

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.

Filters that arrive as strings

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();    // → 1

The 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.

Taking a filter in your own API

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.