A terminal UI and CLI for browsing X, built with Ratatui and the X API v2.
Browse your home timeline, mentions, bookmarks, search tweets, view threads, and look up user profiles — all from your terminal. Includes a non-interactive CLI mode that outputs JSONL to stdout for piping into tools like jq, grep, and wc. Based on the Ratatui event driven async template. Several implementations — including the X API v2 client, authentication strategies, and credential handling — were adapted from x-cli, a command-line client for X.
- Rust (2024 edition) — install via rustup
- An X Developer account with API credentials
cargo install --path .Or build and run without installing:
cargo build --release
cargo runEnable debug logging by setting RUST_LOG:
RUST_LOG=debug cargo run 2>debug.logOptional configuration file at ~/.config/xplorertui/config.toml:
tick_rate_fps = 30 # UI refresh rate
default_max_results = 20 # Tweets per API request (10–100)
default_view = "home" # One of: home, mentions, bookmarks, search
openrouter_callback_port = 3000 # OpenRouter OAuth localhost callback port
# Optional: label :cluster topics with Jev (TypeSafe) instead of a chat LLM,
# and set the model for `classify` / `rank`. Needs OpenRouter auth.
[jev]
model = "typesafe/jev-1.13" # pin a version; thresholds shift between releases
concurrency = 8
# topics = { rust = "Rust language and ecosystem", ai_ml = "AI, ML, LLMs" } # default: built-in taxonomy; `other` is always added
propose_labels = true # with a chat model available, use it for :topics labels (Jev routes; see below)xplorertui supports three auth methods, auto-detected from environment variables (OAuth 2.0 PKCE > OAuth 1.0a > Bearer). The recommended way to supply these variables is fnox. fnox keeps the secrets in a secret store (for example the macOS Keychain) and puts them into the process environment only for the command you run. Plain .env files are still supported (see Using .env files).
-
Install fnox (for example
mise use -g fnox, or see the fnox docs). -
Declare a provider in a
fnox.toml. The repository includes one that uses the macOS Keychain. To use xplorertui from any directory, put the provider in the global config~/.config/fnox/config.tomlinstead:default_provider = "keychain" [providers.keychain] type = "keychain" service = "xplorertui"
-
Store each secret.
fnox setprompts for the value with hidden input, so the secret does not go into your shell history:fnox set X_CLIENT_ID # add -g to write to the global config fnox set X_CLIENT_SECRET # optional for public clients fnox set OPENROUTER_API_KEY # optional, see OpenRouter Integration
-
Run xplorertui through fnox:
fnox exec -- xplorertui auth # one-time OAuth 2.0 PKCE login fnox exec -- xplorertui # TUI fnox exec -- xplorertui home | jq .
To skip the
fnox exec --prefix, enable the shell hook. It loads the secrets when you enter a directory that has afnox.toml:eval "$(fnox activate zsh)" # add to ~/.zshrc; also bash, fish, nu, pwsh
Run fnox check to confirm that all declared secrets resolve. Other providers (1Password, age, cloud secret managers, and more) work the same way. Only the [providers.*] section changes.
The sections below list the variables for each auth method. Store each one with fnox set NAME.
Enables full user-context access (home timeline, mentions, bookmarks). Tokens are persisted at ~/.config/xplorertui/tokens.json and automatically refreshed when they expire.
| Variable | Required |
|---|---|
X_CLIENT_ID |
yes |
X_CLIENT_SECRET |
no (public clients) |
After you store the credentials, authenticate before you launch the TUI:
fnox exec -- xplorertui authThis opens your browser for authorization and saves the tokens. You can also authenticate from within the TUI by typing :auth in command mode.
Full user-context access using HMAC-SHA1 signed requests.
| Variable | Required |
|---|---|
X_CONSUMER_KEY |
yes |
X_CONSUMER_KEY_SECRET |
yes |
X_ACCESS_TOKEN |
yes |
X_ACCESS_TOKEN_SECRET |
yes |
X_BEARER_TOKEN |
no (used for read-only endpoints) |
Read-only access. User-context endpoints (home timeline, mentions, bookmarks) will not be available.
| Variable | Required |
|---|---|
X_BEARER_TOKEN |
yes |
If you do not use fnox, put the same variables in a .env file at one of these locations (highest priority first):
~/.config/xplorertui/.env~/.config/x-cli/.env./.env(current directory)
X_CLIENT_ID=your_client_id
X_CLIENT_SECRET=your_client_secretA variable that is already set in the environment (for example by fnox exec) takes precedence over the same variable in a .env file. Do not commit .env files.
xplorertui integrates with OpenRouter for embedding-powered features: semantic search re-ranking and topic clustering of your timeline.
Supply an OpenRouter API key in one of these ways. When OPENROUTER_API_KEY is set in the environment, it takes precedence over a stored key.
-
fnox (recommended): store the key once, then run through fnox:
fnox set OPENROUTER_API_KEY fnox exec -- xplorertui
-
OAuth PKCE: run
xplorertui openrouter-auth, or type:openrouter-auth(alias:or-auth) in the TUI. The key is saved to~/.config/xplorertui/openrouter_tokens.json. -
.envfile: setOPENROUTER_API_KEY(see Using.envfiles).
Jev features (:topics labels, classify, rank) use the same OpenRouter key. To use Jev for :topics in the TUI, add a [jev] section to ~/.config/xplorertui/config.toml (see Configuration). Without it, :topics asks the selected chat model to write labels. classify and rank use the default Jev settings when the section is not there.
Before using embedding features, select a model:
- Type
:embeddingsin the TUI to open the model selection list - Browse available embedding models with
j/k - Press
Enterto select one (e.g.openai/text-embedding-3-small)
With an OpenRouter client and embedding model configured, search results (/query) are automatically re-ranked by semantic similarity to your query. The original API results are fetched first, then each tweet is embedded and sorted by cosine similarity to the query embedding.
Type :cluster to cluster the tweets in the current view (following, mentions, search, or bookmarks) by topic:
- All tweet texts are embedded via the selected model
- K-means clustering groups tweets into 5 topic clusters
- PCA projects embeddings to 2D coordinates
- A scatter plot is displayed using kuva's terminal backend with Unicode braille characters
If no embedding model is available (the MLX server is not reachable and no OpenRouter embedding model is selected), a prompt offers to open the embedding model picker, or to sign in to OpenRouter first. Clustering starts when you pick a model.
Each cluster is labeled with the tweet closest to its centroid. :topics replaces those placeholders with short topic labels:
- With a
[jev]config section and a chat model (MLX or a selected OpenRouter model), Jev first checks whether each cluster is about technology.- If at least half the posts are in tech clusters, the chat model proposes three candidate labels per cluster and Jev picks the best one for each cluster. No two clusters get the same label, and a cluster that none of the candidates fit shows as "Mixed".
- Otherwise the chat model writes the labels itself, which works better on non-tech timelines.
- Set
propose_labels = falseto skip the chat model.
- With
[jev]and no chat model (or if the chat steps fail), Jev picks one topic per cluster from the fixed taxonomy. Clusters without a clear topic show as "Mixed". On a timeline dominated by one area, several clusters can land on the same broad topic; a finertopicslist helps. - Without
[jev], the chat model writes the labels.
Jev requests go out one per cluster, concurrently. Chat requests turn reasoning off so the short label budget isn't used up by hidden reasoning; models that require reasoning get low-effort reasoning and a larger budget instead.
While labels are generated, the cluster list title names the labeller that is running at that moment (generating labels via MLX + Jev...). It names none during Jev's tech check, and it changes if the request falls back to another labeller.
When a subcommand is provided, xplorertui bypasses the TUI and outputs JSONL (one JSON object per line) to stdout. This makes it easy to pipe X API data into other tools.
xplorertui # Launch TUI (default)
xplorertui tui # Launch TUI (explicit)
xplorertui auth # OAuth 2.0 PKCE flow
xplorertui openrouter-auth # OpenRouter OAuth PKCE flow
xplorertui home # Home timeline → JSONL
xplorertui mentions # Mentions → JSONL
xplorertui bookmarks # Bookmarks → JSONL
xplorertui search <query> # Search tweets → JSONL
xplorertui user <username> # User profile → JSONL
xplorertui open <tweet_id_or_url> # Single tweet + thread → JSONL
xplorertui openrouter-models # List embedding models → JSONL
xplorertui embed <text> -m <model> # Generate embedding → JSON
xplorertui similar <query> -m <model> # Semantic search → ranked JSONL
xplorertui classify < tweets.jsonl # Tag with Jev topic + flags → JSONL
xplorertui rank <query> < tweets.jsonl # Jev relevance re-rank → JSONLEach tweet line is a denormalized JSON object with the tweet, its author, and any attached media embedded:
# Pretty-print your home timeline
xplorertui home | jq .
# Count mentions
xplorertui mentions | wc -l
# Search and filter with jq
xplorertui search "rust lang" | jq '.tweet.text'
# Open a tweet by URL
xplorertui open https://x.com/user/status/1234567890
# Semantic search (re-ranked by embedding similarity)
xplorertui similar "rust async runtime" -m openai/text-embedding-3-small
# Re-rank search results with Jev (a judgment model that reads query and post together)
xplorertui search "rust async" | xplorertui rank "rust async runtimes" | jq -r '.tweet.text'
# Tag your timeline by topic, then keep only questions
xplorertui home | xplorertui classify | jq 'select(.jev.is_question > 0.5)'classify and rank read tweet JSONL on stdin, so they only need OpenRouter credentials (for example fnox exec -- xplorertui home | fnox exec -- xplorertui classify). They send one Decisions request per tweet, [jev].concurrency at a time, and print the total cost to stderr.
| Key | Action |
|---|---|
j / Down |
Move down |
k / Up |
Move up |
Enter |
Open selected item (thread view) |
Esc / q |
Go back / quit |
n |
Load next page |
y |
Copy tweet URL to clipboard |
o |
Open tweet in browser |
m |
Show the full status message (when it is cut off and shows [m]ore) |
j/k, ↑/↓, PgUp/PgDn |
Scroll a popup whose text is longer than the window |
| Key | Action |
|---|---|
1 |
Home timeline |
2 |
Mentions |
3 |
Bookmarks |
4 |
Search |
? |
Help overlay |
| Key | Action |
|---|---|
: |
Command mode |
/ |
Search tweets |
@ |
Look up user |
Ctrl-C |
Quit |
Type : to enter command mode, then:
| Command | Action |
|---|---|
:user <username> |
View a user's profile |
:search <query> |
Search tweets |
:open <url or id> |
Open a tweet by URL or ID |
:home |
Switch to home timeline |
:mentions / :m |
Switch to mentions |
:bookmarks / :b |
Switch to bookmarks |
:auth / :login |
Authenticate with X OAuth 2.0 PKCE |
:openrouter-auth / :or-auth |
Authenticate with OpenRouter |
:embeddings |
Select an embedding model |
:openrouter-models / :openrouter |
Select an OpenRouter chat model |
:cluster |
Cluster home timeline by topic |
:help / :h |
Show help |
:quit / :q |
Quit |
Copyright (c) Jesse Armand jesse@jessearmand.com
This project is licensed under the MIT license (LICENSE or http://opensource.org/licenses/MIT)