| Crates.io | ndl |
| lib.rs | ndl |
| version | 0.2.14 |
| created_at | 2026-01-14 04:47:47.999745+00 |
| updated_at | 2026-01-21 19:29:10.670177+00 |
| description | A minimal TUI client for Threads |
| homepage | |
| repository | https://github.com/pgray/ndl |
| max_upload_size | |
| id | 2042205 |
| size | 316,408 |
A minimal multi-platform TUI client for Threads and Bluesky - stay aware of notifications without the distractions of a full social media interface.

Social media notifications can pull you out of flow state. needle lets you:
Tab keyShift+Ph, j, k, l for intuitive movementThis is a Cargo workspace with two binaries and a shared library:
cargo install ndl
git clone https://github.com/pgray/ndl
cd ndl
cargo build --release --workspace
On Linux, the build uses wild linker for faster builds.
Install with cargo install wild-linker and ensure clang is available.
needle supports both Threads and Bluesky. You can configure one or both platforms.
See OAUTH.md for detailed Threads API setup instructions.
By default, ndl uses the hosted auth server at ndl.pgray.dev - no setup required:
ndl login # Login to Threads
# or
ndl login threads # Explicitly specify Threads
Bluesky uses username/password authentication:
ndl login bluesky
You'll be prompted for:
user.bsky.social) or emailCredentials are saved to ~/.config/ndl/config.json:
{
"access_token": "...",
"bluesky": {
"identifier": "user.bsky.social",
"password": "your-app-password",
"session": "..."
}
}
To use a different auth server:
# Via environment variable
export NDL_OAUTH_ENDPOINT=https://your-ndld-server.com
ndl login
# Or add to ~/.config/ndl/config.json:
# "auth_server": "https://your-ndld-server.com"
If you have your own Threads API credentials and want to run OAuth locally:
# Set empty endpoint to disable hosted auth
export NDL_OAUTH_ENDPOINT=""
export NDL_CLIENT_ID=your_client_id
export NDL_CLIENT_SECRET=your_client_secret
ndl login
ndl logout
ndl --version
Config is stored at ~/.config/ndl/config.json.
If you want to host your own OAuth server:
export NDL_CLIENT_ID=your_client_id
export NDL_CLIENT_SECRET=your_client_secret
export NDLD_PUBLIC_URL=https://your-domain.com # Must match Threads app redirect URI
export NDLD_PORT=8080 # Optional, defaults to 8080
cargo run -p ndld
Automatic TLS certificates via Let's Encrypt:
export NDLD_ACME_DOMAIN=ndl.example.com
export NDLD_ACME_EMAIL=admin@example.com
export NDLD_ACME_DIR=/var/lib/ndld/acme # Optional, for cert persistence
export NDLD_PORT=443
cargo run -p ndld
Set NDLD_ACME_STAGING=1 to use Let's Encrypt staging environment for testing.
export NDLD_TLS_CERT=/path/to/cert.pem
export NDLD_TLS_KEY=/path/to/key.pem
cargo run -p ndld
cp .env.example .env
# Edit .env with your credentials
# Create data directory with correct ownership (ndld runs as UID 10001)
sudo mkdir -p /ndld-data
sudo chown 10001:10001 /ndld-data
docker compose up -d
docker build -f ndld/Dockerfile -t ndld .
docker run -p 8080:8080 \
-e NDL_CLIENT_ID=your_client_id \
-e NDL_CLIENT_SECRET=your_client_secret \
-e NDLD_PUBLIC_URL=https://your-domain.com \
ndld
For Let's Encrypt in Docker:
# Create data directory with correct ownership (ndld runs as UID 10001)
sudo mkdir -p /var/lib/ndld
sudo chown 10001:10001 /var/lib/ndld
docker run -p 443:443 \
-e NDL_CLIENT_ID=your_client_id \
-e NDL_CLIENT_SECRET=your_client_secret \
-e NDLD_PUBLIC_URL=https://your-domain.com \
-e NDLD_PORT=443 \
-e NDLD_ACME_DOMAIN=your-domain.com \
-e NDLD_ACME_EMAIL=admin@your-domain.com \
-v /var/lib/ndld:/var/lib/ndld \
ndld
For manual TLS in Docker:
docker run -p 443:443 \
-e NDL_CLIENT_ID=your_client_id \
-e NDL_CLIENT_SECRET=your_client_secret \
-e NDLD_PUBLIC_URL=https://your-domain.com \
-e NDLD_PORT=443 \
-e NDLD_TLS_CERT=/certs/cert.pem \
-e NDLD_TLS_KEY=/certs/key.pem \
-v /path/to/certs:/certs:ro \
ndld
The server exposes:
GET / - Landing page with project infoGET /privacy-policy - Privacy policyGET /tos - Terms of servicePOST /auth/start - Start OAuth sessionGET /auth/callback - OAuth callback (configure in Threads app)GET /auth/poll/{session_id} - Poll for auth completionGET /health - Health checkndl
When you have multiple platforms configured, ndl automatically enters multi-platform mode. You'll see platform indicators in the status bar (e.g., [Threads] Bluesky) showing which platform is currently active (in brackets).
Tab to toggle between configured platformsShift+P to post to all platforms simultaneously| Key | Action |
|---|---|
j/Down |
Move down |
k/Up |
Move up |
h/Left |
Focus threads panel |
l/Right |
Focus detail panel |
t |
Swap panel positions |
p |
Post new thread |
P |
Cross-post to all platforms |
r |
Reply to selected thread |
R |
Refresh feed |
Tab/] |
Switch platform (multi-platform) |
Enter |
Select / focus detail |
Esc |
Back / cancel |
? |
Toggle help |
q |
Quit |
gh workflow run release.yml -f version=X.Y.Z
This bumps versions, creates a tag, builds binaries, publishes to crates.io, and pushes the Docker image.
ndl and ndld do not track, collect, or store any personal information. See PRIVACY.md for details.
MIT