The explorer TUI

lairs tui provides four views of Layers data: Explore searches a local corpus index, Discover finds and indexes datasets from configured sources, Browse presents type-specific views of records in a local repository, and Query runs queries over materialized data.

lairs tui --index path/to/index --repo path/to/repo --data path/to/materialized

All paths are optional. --index opens a discovery index (built with lairs index build) on the Explore tab; --repo opens a local repository (built with lairs pull) on the Browse tab; --data opens a directory of materialized Parquet views on the Query tab. A tab with no source shows guidance on how to produce one. The TUI opens on whichever tab fits the source you gave: a repository opens on Browse, a bare materialized directory on Query, and otherwise the Explore index.

With no --index, the TUI uses a default index location (under the XDG state directory, overridable with LAIRS_INDEX_DIR) and, on launch, crawls the configured sources and indexes every newly discovered dataset that you have not muted, which populates the Explore tab. Pass --no-auto-index to skip the launch crawl and index datasets from the Discover tab instead.

--repo also feeds the Query tab: the repository is flattened into Parquet views in a scratch directory, so lairs tui --repo path/to/repo gives you both a type-aware Browse and a Query workbench from one source. Pass --data explicitly to query a different directory.

Press 1, 2, 3, and 4 to switch to Explore, Browse, Query, and Discover, ctrl+r to run a query, ctrl+s to open settings, ctrl+t to cycle the color theme, f1 for help, and ctrl+q to quit.

Explore

The discovery index is an index of corpora: lairs index build crawls repositories and records one card per pub.layers.corpus.corpus. The Explore tab lists every indexed corpus, one per row. Type in the facet boxes (search text, domain, language, license, minimum expression count) to filter and re-rank the list live; the highlighted corpus's card, with its description, languages, license, annotation design, ontologies, and linked eprints, renders in the detail panel on the right.

The facet boxes build the same SearchQuery used by lairs index search, and the same scorer orders the results.

Discover

Where Explore reads the index you already have, Discover finds datasets to add to it. It lists the configured sources on the left; pick one to crawl it (press enter, or r to re-crawl). The crawl runs in the background and fills the table on the right with the datasets it finds, one per row, each tagged with its state:

Press enter or space on a row to toggle it: a new (or muted) dataset becomes indexed and appears on the Explore tab; an indexed dataset becomes muted and is dropped from the index. Muting persists across crawls: neither a later crawl nor launch auto-indexing will re-index the dataset until you unmute it.

Sources

A source is a PDS or relay endpoint lairs crawls for datasets. lairs ships a built-in default for the public Layers PDS (repo.layers.pub), which is deliberately kept off the firehose and so would otherwise be undiscoverable. Sources are configured in a sources.toml file under the XDG config directory (~/.config/lairs/sources.toml, overridable with LAIRS_SOURCES_FILE):

[[source]]
name = "my-pds"
endpoint = "https://pds.example"
kind = "pds"

[[source]]
name = "layers-pub"
enabled = false

Each [[source]] adds a source by name and endpoint; a kind of pds or relay defaults to pds. An entry whose name matches a built-in overrides that built-in's fields. The second entry above disables the built-in repo.layers.pub without redefining it. lairs sources list prints the resolved sources (add --json for machine-readable output), and lairs index build --source <name> crawls a named source instead of a bare --endpoint. With neither flag, lairs index build crawls the first enabled source, so a default install indexes repo.layers.pub with no configuration.

Settings

Press ctrl+s to open the settings modal. It lists the configured sources and every dataset you have muted, with when and where each was muted. Highlight a muted dataset and press u (or space) to unmute it, so a later crawl or the launch auto-index can pick it up again. Press esc to close.

Browse

Where Explore is corpus-centric, Browse covers every kind of Layers data. It reads a local repository (the output of lairs pull) and selects views by record type. The pane has three panels:

Switching views

The detail panel shows one visualization at a time. Press v to flip between the views that fit the highlighted record; the panel title names the current view and its position (for instance Tree, 3/5), and when more than one view is available it adds a v: switch view hint. A Detail view appears last in the cycle and exposes every field. A record offers only views for which it has content.

The views are model-driven: they dispatch on the lexicon's own type system, selecting a view by annotation kind/subkind, judgment taskType, and graph edgeType, and resolving each annotation's anchor (text byte span, token reference, temporal span, ...) inside the chosen view. Any conforming repository uses the same dispatch, which is not tied to the reference corpora. The visualizations follow established text-mode conventions:

Type in the filter box above the record list to narrow it by any visible cell.

Query

The Query tab runs over a directory of materialized Parquet views, and is not limited to corpora. The engine registers every *.parquet file in the directory as a view named after its file stem, so any materialized Layers records are queryable through the same SQL: Corpus.materialize writes the expressions and annotations views; ontologies, resource collections, experiments, relation graphs, and media become queryable once their records are materialized to Parquet alongside them.

The schema browser on the left lists every view and its columns; click a table or column to insert its name into the editor. Choose a mode, write a query, and press Run (ctrl+r). The three query modes differ in their syntax and scope.

SQL

DuckDB SQL runs over every materialized view. It supports joins across record types, aggregation, window functions, regular expressions, and full-text search. The schema browser shows the tables and columns available in the materialized data.

SELECT label, count(*) AS n
FROM annotations
GROUP BY label
ORDER BY n DESC

Concordance

A keyword-in-context (KWIC) search. The query is a regular expression matched against a text column (expressions.text by default); each hit becomes a row of the source identifier, the left context, the matched text, and the right context, so results read as a classic concordance.

\brun(s|ning)?\b

CQL

A corpus query language over token-aligned annotations (the annotations view). A query is a sequence of bracketed token constraints; adjacent blocks match consecutive token positions within the same layer, so the pattern matches token sequences. A constraint is attribute operator "value", where the operator is = (exact), != (negated), or ~ (regular expression); an empty [] matches any token.

[label="DET"] [label="ADJ"] [label="NOUN"]
[label~"^V" & value!="be"]

The attributes are the columns of the annotations view, which are the scalar fields of each annotation (label, value, text, confidence, tokenIndex, headIndex, targetIndex, ontologyTypeRef) plus layer_uri, annotation_index, and the flattened anchor columns. Layer-level fields such as subkind are not columns of this view, so they are not available as CQL attributes; an unknown attribute is rejected. The available vocabulary of values follows whatever the data was annotated with.

Producing the inputs

# a discovery index of corpora for the Explore tab
lairs index build --into index --endpoint wss://relay.example

# a local repository of an account's records for the Browse tab
lairs pull <did> --endpoint https://pds.example --into repo

# materialize a corpus for the Query tab
lairs materialize <corpus-at-uri> --endpoint https://pds.example --out materialized

lairs tui --index index --repo repo --data materialized

The Browse tab needs only the repository, and feeds the Query tab from it too:

lairs pull <did> --endpoint https://pds.example --into repo
lairs tui --repo repo

The query engine is also usable on its own, without the terminal UI, over any directory of Parquet views:

from pathlib import Path
from lairs.tui import QueryEngine

engine = QueryEngine.open(Path("materialized"))
print(engine.tables)  # the views available to query
result = engine.run_sql("SELECT * FROM annotations LIMIT 20")
for row in result.rows:
    print(row.cells)