Explorer TUI

The interactive terminal user interface for discovering, browsing, and querying Layers data. The lairs.tui package is a Textual application with three surfaces: an Explore screen over the discovery index, a type-aware Browse screen over a local repository, and a Query workbench over materialized Parquet views. The pure-Python query engine is usable on its own, without the terminal stack. For usage see Guides > The explorer TUI.

Application

run_tui launches the full application, and QueryEngine is the DuckDB-backed query engine that the Query screen drives. The query result models (QueryResult, QueryRow) and errors (QueryError, CqlError) round out the public surface of the package.

lairs.tui

Interactive terminal UI for exploring and querying Layers data.

The TUI is a colorful Textual application with three surfaces: an Explore screen that browses and filters the discovery index, a Browse screen that explores every record in a local repository through type-aware views, and a Query screen that runs powerful searches over materialized data through three query modes (DuckDB SQL, a KWIC concordance, and a CQL token-pattern language). The pure-Python query engine in :mod:lairs.tui.query is usable on its own; :func:run_tui launches the full application.

CqlError

Bases: QueryError

Raised when a CQL token-pattern fails to parse against the schema.

QueryEngine

QueryEngine(
    connection: DuckDBPyConnection, tables: Sequence[str]
)

A DuckDB-backed query engine over materialized corpus views.

PARAMETER DESCRIPTION
connection

The DuckDB connection holding the registered views.

TYPE: DuckDBPyConnection

tables

The registered view names, in display order.

TYPE: collections.abc.Sequence of str

tables property

tables: tuple[str, ...]

Return the registered view names in display order.

open classmethod

open(data_dir: Path) -> QueryEngine

Open a directory of Parquet views as a queryable engine.

Every *.parquet file in data_dir is registered as a DuckDB view named after its file stem, so expressions.parquet becomes the expressions view.

PARAMETER DESCRIPTION
data_dir

A directory of materialized Parquet views.

TYPE: Path

RETURNS DESCRIPTION
QueryEngine

An engine over the views found in the directory.

RAISES DESCRIPTION
QueryError

When the directory does not exist or holds no Parquet views.

columns

columns(table: str) -> tuple[str, ...]

Return the column names of a registered view.

PARAMETER DESCRIPTION
table

A registered view name.

TYPE: str

RETURNS DESCRIPTION
tuple of str

The view's column names, or an empty tuple when it is unknown.

schema

schema() -> tuple[tuple[str, tuple[str, ...]], ...]

Return every view with its columns, for the schema browser.

RETURNS DESCRIPTION
tuple of (str, tuple of str)

Pairs of view name and its column names, in display order.

run_sql

run_sql(
    sql: str, *, limit: int = _DEFAULT_ROW_LIMIT
) -> QueryResult

Execute read-only DuckDB SQL and return a bounded result.

PARAMETER DESCRIPTION
sql

The SQL statement to run.

TYPE: str

limit

The maximum number of rows to return; one extra row is fetched to detect truncation.

TYPE: int DEFAULT: _DEFAULT_ROW_LIMIT

RETURNS DESCRIPTION
QueryResult

The columns, rows, count, and truncation flag.

RAISES DESCRIPTION
QueryError

When the statement is empty or DuckDB rejects it.

concordance

concordance(
    pattern: str,
    *,
    table: str = "expressions",
    column: str = "text",
    window: int = _DEFAULT_KWIC_WINDOW,
    ignore_case: bool = True,
    limit: int = _DEFAULT_ROW_LIMIT,
) -> QueryResult

Run a keyword-in-context (KWIC) search over a text column.

Each match of the regular expression pattern yields a row of the source identifier, the left context window, the matched text, and the right context window, so hits read as a classic concordance.

PARAMETER DESCRIPTION
pattern

A Python regular expression to search for.

TYPE: str

table

The view holding the text column.

TYPE: str DEFAULT: 'expressions'

column

The text column to search.

TYPE: str DEFAULT: 'text'

window

The number of context characters to show on each side of a match.

TYPE: int DEFAULT: _DEFAULT_KWIC_WINDOW

ignore_case

Whether to match case-insensitively.

TYPE: bool DEFAULT: True

limit

The maximum number of concordance lines to return.

TYPE: int DEFAULT: _DEFAULT_ROW_LIMIT

RETURNS DESCRIPTION
QueryResult

Columns source, left, match, right.

RAISES DESCRIPTION
QueryError

When the table or column is unknown, or the pattern is invalid.

cql

cql(
    query: str,
    *,
    table: str = "annotations",
    limit: int = _DEFAULT_ROW_LIMIT,
) -> QueryResult

Compile and run a CQL token-pattern over the annotations table.

A CQL query is a sequence of bracketed token constraints, for example [label="DET"] [label="NOUN"] or [subkind="pos" & label="VERB"]. Each constraint matches an annotation attribute (= exact, != negated, ~ regular expression); an empty [] matches any token. Adjacent blocks are joined on consecutive token_index within the same layer, so the pattern matches token sequences. Each block matches exactly one token; repetition quantifiers (*, +, ?, {...}) are not supported and a pattern that uses one raises :class:CqlError.

PARAMETER DESCRIPTION
query

The CQL token-pattern.

TYPE: str

table

The token-aligned annotations view to query.

TYPE: str DEFAULT: 'annotations'

limit

The maximum number of matches to return.

TYPE: int DEFAULT: _DEFAULT_ROW_LIMIT

RETURNS DESCRIPTION
QueryResult

Columns layer_uri, token_index, and one tok{i} per block.

RAISES DESCRIPTION
CqlError

When the pattern is empty, malformed, or references unknown columns.

QueryError

When DuckDB rejects the compiled statement.

close

close() -> None

Close the underlying DuckDB connection.

QueryError

Bases: Exception

Raised when a query fails to execute or compile.

QueryResult

Bases: Model

A tabular query result with its columns, rows, and run metadata.

ATTRIBUTE DESCRIPTION
columns

The result column names.

TYPE: tuple of str

rows

The result rows, each a tuple of stringified cells.

TYPE: tuple of QueryRow

row_count

The number of rows returned (after any display truncation).

TYPE: int

truncated

Whether more rows matched than were returned.

TYPE: bool

elapsed_ms

Wall-clock execution time in milliseconds.

TYPE: float

QueryRow

Bases: Model

One result row as a tuple of stringified cells.

ATTRIBUTE DESCRIPTION
cells

The row's cell values, rendered to strings for display.

TYPE: tuple of str

run_tui

run_tui(
    *,
    index_path: str | None = None,
    data_path: str | None = None,
    repo_path: str | None = None,
) -> None

Launch the Layers explorer TUI.

PARAMETER DESCRIPTION
index_path

Filesystem path to a discovery index directory to open on the Explore screen. When omitted the Explore screen starts empty.

TYPE: str or None DEFAULT: None

data_path

Filesystem path to a directory of materialized Parquet views to open on the Query screen. When omitted, a repository given by repo_path is materialized to feed the Query screen; otherwise it starts empty.

TYPE: str or None DEFAULT: None

repo_path

Filesystem path to a local Repository to open on the Browse screen. When given without data_path, the repository is also flattened to back the Query screen.

TYPE: str or None DEFAULT: None

Visualizations

Text-mode renderers that turn Layers records into terminal-friendly views: interlinear token tags, CoNLL-U grids, dependency trees, brat-style span overlays, judgment distributions, tier timelines, alignment bitexts, and the anchor and syntax helpers they share.

lairs.tui.viz

Model-driven visualizers for Layers annotations, judgments, and structure.

Everything here dispatches on the lexicon's own type system, not on any dataset: annotation kind/subkind/anchor, judgment taskType, graph nodeType/ edgeType. The four reference corpora only exercise some paths; a conforming record of any provenance renders through the same dispatch, with custom and URI-valued slots degrading to a faithful generic view.

Renderings are monospace text (wrapped in fenced code blocks by the caller's view layer) following established text-mode conventions: CoNLL-U token grids, indented and arc dependency trees, bracketed constituency trees, brat-style span lanes, ELAN-style tier timelines, Penman-ish graph edges, and sparkline / diverging-bar / number-line judgment distributions.

Token

Token(
    index: int, text: str, byte_start: int, byte_end: int
)

One token with its surface form and offsets within an expression.

Syntax

Syntax(
    tokens: list[Token],
    columns: list[str],
    tags: dict[str, dict[int, str]],
    deps: dict[int, tuple[int, str]],
    tokenization: str,
)

The token-aligned annotations assembled over one tokenization.

feature_map

feature_map(value: JsonValue) -> dict[str, str]

Flatten a featureMap ({entries:[{key,value}]}) to a dict.

anchor_token_indexes

anchor_token_indexes(anchor: JsonValue) -> list[int]

Return the token indexes an anchor references.

Handles tokenRef (single) and tokenRefSequence (possibly non-contiguous); other anchor variants reference no tokens.

anchor_tokenization

anchor_tokenization(anchor: JsonValue) -> str

Return the tokenization id a token anchor refers to, or "".

anchor_byte_span

anchor_byte_span(
    anchor: JsonValue,
) -> tuple[int, int] | None

Return the (byteStart, byteEnd) of a text or page anchor, or None.

anchor_time_span

anchor_time_span(
    anchor: JsonValue,
) -> tuple[int, int] | None

Return the (start_ms, end_ms) of a temporal anchor, or None.

anchor_summary

anchor_summary(anchor: JsonValue) -> str

Return a one-line human descriptor of any anchor variant.

tokenizations_of

tokenizations_of(
    browser: RepoBrowser, expr_uri: str
) -> dict[str, list[Token]]

Return tokenizationId -> ordered tokens for an expression.

Reads every segmentation whose expression is this expression; a token's surface comes from its text when present, else from slicing the expression text by the token's textSpan byte offsets.

layers_of

layers_of(
    browser: RepoBrowser, expr_uri: str
) -> list[tuple[str, Mapping[str, JsonValue]]]

Return the annotation layers anchored to an expression.

sparkline

sparkline(counts: Sequence[float]) -> str

Render a sequence of magnitudes as an eight-level block sparkline.

hbar

hbar(value: float, peak: float, width: int = 24) -> str

Render a horizontal bar of value scaled against peak.

segmentation_tokens

segmentation_tokens(
    browser: RepoBrowser, data: Mapping[str, JsonValue]
) -> str

Render a segmentation as a readable token table per tokenization.

assemble_syntax

assemble_syntax(
    browser: RepoBrowser, expr_uri: str
) -> Syntax | None

Assemble token-tag and dependency layers over an expression's tokens.

Returns None when the expression has no tokenization to anchor into.

single_dep_syntax

single_dep_syntax(
    browser: RepoBrowser,
    expr_uri: str,
    layer: Mapping[str, JsonValue],
) -> Syntax | None

Assemble a Syntax holding just one dependency layer's relations.

token_tag_interlinear

token_tag_interlinear(
    browser: RepoBrowser,
    expr_uri: str,
    layer: Mapping[str, JsonValue],
) -> str

Render a single token-tag layer as an interlinear strip over the tokens.

conllu_grid

conllu_grid(syntax: Syntax) -> str

Render the token-aligned tags and dependencies as a CoNLL-U style grid.

dependency_tree

dependency_tree(syntax: Syntax) -> str

Render dependencies as an indented head-to-dependent tree.

interlinear

interlinear(
    tokens: list[Token],
    rows: list[tuple[str, dict[int, str]]],
) -> str

Render token-tag layers as interlinear rows beneath each token.

rows is ordered (row_label, {tokenIndex: value}) pairs; columns align on token index and wrap to a readable width, the classic Leipzig layout.

span_overlay

span_overlay(
    text: str, spans: list[tuple[int, int, str]]
) -> str

Render labeled character spans as brat-style underlines over the text.

spans is (charStart, charEnd, label); overlapping spans stack onto separate lanes so nesting stays legible.

span_layer_overlay

span_layer_overlay(
    browser: RepoBrowser,
    expr_uri: str,
    layer: Mapping[str, JsonValue],
) -> str

Render a span-kind layer as labeled underlines over the expression text.

Token-anchored spans resolve to character ranges through the tokenization; byte-anchored spans convert directly. Dataset-agnostic over the anchor union.

tier_timeline

tier_timeline(
    tiers: list[tuple[str, list[tuple[int, int, str]]]],
    *,
    ms_per_col: int = 200,
) -> str

Render time-aligned tiers as an ELAN-style partitur grid.

tiers is (tier_name, [(startMs, endMs, label)]); a shared time ruler sits on top and each tier is a lane whose boundaries align by column.

document_tags

document_tags(anns: list[Mapping[str, JsonValue]]) -> str

Render document-level annotations as labeled chips.

Confidence is the lexicon's integer score scaled 0-1000 (1000 = maximum) and is shown as a percentage of that range.

graph_edges

graph_edges(
    nodes: Mapping[str, Mapping[str, JsonValue]],
    edges: list[Mapping[str, JsonValue]],
) -> str

Render graph edges as a source --type--> target adjacency list.

Node references resolve to a readable label through nodes (keyed by AT-URI) when present, else fall back to the ref's most specific id.

alignment_bitext

alignment_bitext(
    browser: RepoBrowser, data: Mapping[str, JsonValue]
) -> str

Render an alignment as source-to-target index links with surfaces.

judgment_distribution

judgment_distribution(
    experiment: Mapping[str, JsonValue],
    judgments: list[Mapping[str, JsonValue]],
) -> str

Render a per-item judgment distribution dispatched on taskType.

Record views

Model-driven view dispatch over the lexicon's own type system: the views available for a record, the columns for a record list, and the rendering of a record through a single focused view at a time.

lairs.tui.views

Type-aware rendering of Layers records for the Browse tab.

:func:render_record dispatches on a record's collection NSID to a Markdown view that fits the data: an ontology renders as a type hierarchy, an experiment as a design plus its response sets, a graph as relation cards, a lexicon collection as its entries, an annotation layer as annotations over text, and so on. Records without a bespoke view fall back to a clean key-value rendering, so every record type is viewable.

Everything reads the record's raw JSON (JsonValue) rather than typed attributes, so the helpers stay concretely typed. :data:LIST_COLUMNS and :func:summarize drive the records list with type-appropriate columns.

record_views

record_views(
    browser: RepoBrowser,
    nsid: str,
    uri: str,
    data: Mapping[str, JsonValue],
) -> list[tuple[str, Callable[[], str]]]

Return the ordered (label, render) views available for a record.

Each view is a single focused visualization; the caller flips between them. Only views with content for this record are offered, so the panel stays uncluttered. A Detail view is always last so every field stays reachable.

view_modes

view_modes(
    browser: RepoBrowser,
    nsid: str,
    uri: str,
    data: Mapping[str, JsonValue],
) -> list[str]

Return the labels of the views available for a record.

render_view

render_view(
    browser: RepoBrowser,
    nsid: str,
    uri: str,
    data: Mapping[str, JsonValue],
    mode: str,
) -> str

Render one named view of a record, falling back to the first view.

render_record

render_record(
    browser: RepoBrowser,
    nsid: str,
    uri: str,
    data: Mapping[str, JsonValue],
) -> str

Render a record's first (default) view.

columns_for

columns_for(nsid: str) -> tuple[str, ...]

Return the records-list columns for a record type, or a generic default.

summarize

summarize(
    nsid: str, uri: str, data: Mapping[str, JsonValue]
) -> tuple[str, ...]

Return a one-row summary of a record matching :func:columns_for.

PARAMETER DESCRIPTION
nsid

The record's collection NSID.

TYPE: str

uri

The record's AT-URI, used by the generic summary.

TYPE: str

data

The record's raw JSON.

TYPE: Mapping

RETURNS DESCRIPTION
tuple of str

The cell values for the record's row.

Record registry

The map from pub.layers.* NSIDs to generated record models, with the namespace and short-label helpers that group records in the Browse type tree.

lairs.tui.registry

The record-type registry: a map from collection NSID to its model class.

The browser needs to decode any pub.layers.* record into a typed model, and to present record types in a stable, readable order grouped by namespace. This module is the single source of both. RECORD_MODELS is exhaustive over the record (not method or permission-set) lexicons; a test cross-checks it against the generated modules so it cannot silently drift.

namespace_of

namespace_of(nsid: str) -> str

Return the namespace segment of a pub.layers.<ns>.<record> NSID.

label_of

label_of(nsid: str) -> str

Return the short record label (the last dotted segment) of an NSID.

Screen panes

The three composable panes that make up the application: an ExplorePane over the discovery index, a BrowsePane over a local repository, and a QueryPane over the materialized views.

lairs.tui.screens

Screens and panes for the lairs explorer TUI.

BrowsePane

BrowsePane(repo_path: str | None)

Bases: Horizontal

A repository record browser with a type tree, list, and detail view.

PARAMETER DESCRIPTION
repo_path

Filesystem path to a local Repository, or None to start empty with on-screen guidance.

TYPE: str or None

compose

compose()

Compose the type tree, records table, and detail panel.

on_mount

on_mount() -> None

Open the repository and build the type tree.

on_tree_node_selected

on_tree_node_selected(event: NodeSelected) -> None

Load the records of the selected type into the records table.

on_input_changed

on_input_changed(event: Changed) -> None

Re-filter the records table as the filter box changes.

on_data_table_row_highlighted

on_data_table_row_highlighted(
    event: RowHighlighted,
) -> None

Render the highlighted record in the detail panel.

action_cycle_view

action_cycle_view(step: int = 1) -> None

Flip to the next (or previous) view of the current record.

ExplorePane

ExplorePane(index_path: str | None)

Bases: Horizontal

A discovery-index browser with facet filters and a detail card.

PARAMETER DESCRIPTION
index_path

Filesystem path to a discovery index directory, or None to start empty with on-screen guidance.

TYPE: str or None

compose

compose()

Compose the filter bar, results table, and detail panel.

on_mount

on_mount() -> None

Populate the table header, load the index, and run the first filter.

on_input_changed

on_input_changed(event: Changed) -> None

Re-filter whenever any facet input changes.

on_data_table_row_highlighted

on_data_table_row_highlighted(
    event: RowHighlighted,
) -> None

Show the highlighted dataset's card in the detail panel.

QueryPane

QueryPane(data_path: str | None)

Bases: Horizontal

A three-mode query workbench over a materialized corpus.

PARAMETER DESCRIPTION
data_path

Filesystem path to a directory of materialized Parquet views, or None to start empty with on-screen guidance.

TYPE: str or None

compose

compose()

Compose the schema tree, mode bar, editor, actions, and results.

on_mount

on_mount() -> None

Open the engine, build the schema tree, and seed the SQL starter.

on_unmount

on_unmount() -> None

Close the DuckDB connection held by the query engine, if any.

The pane owns the engine for its lifetime; closing on unmount releases the DuckDB connection rather than leaking it until process exit.

on_radio_set_changed

on_radio_set_changed(event: Changed) -> None

Seed the starter query when the mode changes.

on_button_pressed

on_button_pressed(event: Pressed) -> None

Run the query when the Run button is pressed.

on_tree_node_selected

on_tree_node_selected(event: NodeSelected) -> None

Insert a selected table or column name at the editor cursor.

A separating space is added when the cursor does not already sit at a token boundary, so an identifier never fuses with the preceding text; the cursor follows the insertion.

action_run

action_run() -> None

Execute the editor's text in the current mode and show the result.