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.
// Given: Server server
server.newSession("build").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.
Every field and relation these four classes expose, generated from
field-catalog.tsv:
| Owner | Field | Kind | tmux format |
|---|---|---|---|
| Pane | id |
TEXT | pane_id |
| Pane | command |
TEXT | pane_current_command |
| Pane | index |
NUMBER | pane_index |
| Pane | active |
FLAG | pane_active |
| Pane | title |
TEXT | pane_title |
| Pane | path |
TEXT | pane_current_path |
| Pane | width |
NUMBER | pane_width |
| Pane | height |
NUMBER | pane_height |
| Pane | left |
NUMBER | pane_left |
| Pane | top |
NUMBER | pane_top |
| Pane | atTop |
FLAG | pane_at_top |
| Pane | atBottom |
FLAG | pane_at_bottom |
| Pane | atLeft |
FLAG | pane_at_left |
| Pane | atRight |
FLAG | pane_at_right |
| Session | id |
TEXT | session_id |
| Session | name |
TEXT | session_name |
| Session | attached |
FLAG | session_attached |
| Session | windowCount |
NUMBER | session_windows |
| Session | windows |
to-many:Window | — |
| Window | id |
TEXT | window_id |
| Window | name |
TEXT | window_name |
| Window | index |
NUMBER | window_index |
| Window | active |
FLAG | window_active |
| Window | linked |
FLAG | window_linked |
| Window | width |
NUMBER | window_width |
| Window | height |
NUMBER | window_height |
| Window | paneCount |
NUMBER | window_panes |
| Window | panes |
to-many:Pane | — |
| Window | session |
to-one:Session | — |
| Client | name |
TEXT | client_name |
| Client | session |
to-one:Session | — |
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 to lower a safe subset to tmux's own -f
predicate. TmuxFilters.format does that lowering. A relation, or an operand
containing ,, #, {, }, or :, stays empty, and the caller filters the
capture it already holds. Server.session(String) and Server.pane(PaneId)
use a targeted listing when the name or id is safe to put in a format, and a
whole-server capture otherwise. sessions(FilterExpr), windows(FilterExpr),
and panes(FilterExpr) send a safe expression as list-sessions -f,
list-windows -f, or list-panes -f, and still apply the expression to what
comes back. Each costs two tmux commands, as a snapshot does, and reads only the
sessions it keeps. A relation, or an expression tmux cannot apply, reads the
whole server.
Filtering a list already in hand still issues no commands.
The optional libtmux-jackson module gives an expression a versioned wire form.
This snippet is exercised by FilterJsonTest rather than
DocumentationSnippetsTest, since the core suite does not depend on Jackson:
String json = FilterJson.writeString(
Pane_.command().startsWith("nv"), LibTmuxModels.pane());
FilterExpr<Pane> restored = FilterJson.readString(json, LibTmuxModels.pane());
restored.describe(); // → pane_current_command starts-with nvOnly exact handles declared by the supplied model can be written. A field may borrow a declared name while carrying a different accessor, so the name alone has no wire identity. Refusing it is what makes this a format rather than a hope.
Writing and reading are validated against a model: a document claiming pane
cannot be read as a FilterExpr<Window>, and an expression cannot borrow a field
or relation name its model did not declare. Unknown schema versions, models,
fields, relations, operators, properties 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. That makes most of the document independent of
Java names. The matches operand is the exception: its syntax and numeric flags
are those of java.util.regex.Pattern. A non-Java consumer must reproduce those
semantics or reject that operator.
An application that stores a pane predicate is the worked example. The wire form is one of these documents:
{"schema": "libtmux.filter/1", "model": "pane",
"expr": {"node": "compare", "field": "pane_current_command",
"op": "starts_with", "value": "nvim"}}Java applications can read that document with the matching FilterModel and
apply it to a captured hierarchy. libtmux-mcp deliberately does not accept
this open expression format: list_panes returns bounded typed metadata for a
client to filter, while search_panes searches only rendered terminal text.
A CLI flag, a config file or a stored query carries a filter as untrusted text.
Read it with the wire form above: FilterJson.readString checks the document
against a model and refuses any field, relation or operator the model did not
declare, so a wrong name fails closed rather than being guessed. The core takes
no string form of its own, which keeps the typed expression the only thing the
rest of the library accepts.
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.