Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ A terminal UI for browsing and posting to Discourse forums. It behaves like a li
- Like/unlike posts.
- Search posts and jump directly to the matching topic context.
- View notifications in a dedicated list and jump straight to the related topic/post.
- Live unread notification badge in the status bar, with MessageBus updates.
- Inline composer with cursor movement, line breaks, and a live character counter.
- Emoji replacements for common `:emoji:` tokens and `:)`-style smiles.
- YAML-driven themes (`default`, `slate`, `fairground`, `rust`) with per-color overrides.
- Inline image previews in expanded posts (uses `chafa`, falls back to `viu`).
- UI localization with built-in `en`, `fr`, `de`, and `es`.
- Username/email + password login (cookie-based session login; supports TOTP/backup codes).
- API key + username login (fallback for SSO-only or locked-down sites).

Expand All @@ -33,6 +35,7 @@ A terminal UI for browsing and posting to Discourse forums. It behaves like a li
| Likes | Full | Like/unlike from Topic View. |
| Search | Full | Search results open directly into matching topic/post context. |
| Notifications | Full | Dedicated notifications list with direct open into the related topic/post. |
| Localization | Full | Built-in `en`, `fr`, `de`, and `es`, selectable by `--lang` or `TERMCOURSE_LANG`. |
| Theming | Full | Built-in themes plus YAML overrides. |
| Inline images | Full | `chafa` primary, `viu` fallback/override. |
| Live list update notification | Partial | Uses Discourse MessageBus channels and shows `New/updated (n)` in the topic-list status area. Current implementation tracks core list filters only; category/tag-scoped refinement is planned. |
Expand All @@ -46,13 +49,20 @@ bundle install

# Option A: username/password login
DISCOURSE_USERNAME="you@example.com" DISCOURSE_PASSWORD="your_password" \
bundle exec bin/termcourse --theme slate https://your.discourse.host
bundle exec bin/termcourse --theme slate --lang fr https://your.discourse.host

# Option B: API key fallback
DISCOURSE_API_KEY="your_key" DISCOURSE_API_USERNAME="your_username" \
bundle exec bin/termcourse --theme fairground https://your.discourse.host
```

Language selection:

```bash
bundle exec bin/termcourse --lang de https://your.discourse.host
TERMCOURSE_LANG=es bundle exec bin/termcourse https://your.discourse.host
```

## Auth

### Option A: Username + Password (recommended for portability)
Expand Down Expand Up @@ -84,9 +94,11 @@ You can set any of these in your shell or `.env` file. `.env` is auto-loaded if
- `TERMCOURSE_HTTP_DEBUG`: Set to `1` to log HTTP/auth debug responses to `/tmp/termcourse_http_debug.txt`.
- `TERMCOURSE_LINKS`: Set to `0` to disable OSC8 clickable links.
- `TERMCOURSE_THEME`: Theme name (default `default`). Built-ins: `default`, `slate`, `fairground`, `rust`.
- `TERMCOURSE_LANG`: UI language: `en`, `fr`, `de`, or `es`. If unset, termcourse uses `LANG`/`LC_*` and falls back to `en`.
- `TERMCOURSE_COLOR_MODE`: UI color mode: `auto` (default), `truecolor`, `256`, `16`. `auto` uses `256` on macOS and `truecolor` elsewhere.
- `TERMCOURSE_THEME_FILE`: Optional path to theme YAML. If unset, termcourse checks `./theme.yml` first, then `~/.config/termcourse/theme.yml`.
- CLI override: `--theme NAME` applies only to the current run and overrides `TERMCOURSE_THEME`.
- CLI override: `--lang LANG` applies only to the current run and overrides locale env detection.
- `TERMCOURSE_IMAGES`: Set to `0` to disable inline image previews.
- `TERMCOURSE_IMAGE_BACKEND`: Choose image backend: `auto` (default), `chafa`, `viu`, or `off`.
- `TERMCOURSE_IMAGE_MODE`: Generic image mode for both `chafa` and `viu`: `stable` (default) or `quality`.
Expand All @@ -99,6 +111,14 @@ You can set any of these in your shell or `.env` file. `.env` is auto-loaded if
- `TERMCOURSE_EMOJI`: Set to `0` to disable emoji substitutions.
- `TERMCOURSE_CREDENTIALS_FILE`: Optional path to host-mapped YAML credentials. If unset, termcourse checks `./credentials.yml` first, then `~/.config/termcourse/credentials.yml`.

## Localization

- Supported UI languages: `en`, `fr`, `de`, `es`.
- Per-run override: `--lang LANG`
- Session/environment default: `TERMCOURSE_LANG=LANG`
- Fallback detection order: `TERMCOURSE_LANG`, then `LC_ALL`, then `LC_MESSAGES`, then `LANG`, then `en`.
- Discourse-provided content such as category names, notification type data, topic titles, and post bodies is shown as returned by the server.

Auth selection order:
- CLI flags (`--username`, `--password`, `--api-key`, `--api-username`) have highest priority.
- Then host credentials from YAML using lookup order: `TERMCOURSE_CREDENTIALS_FILE` path if set, else `./credentials.yml`, else `~/.config/termcourse/credentials.yml`.
Expand Down Expand Up @@ -204,6 +224,7 @@ Color translation:

The status bar shows the current list filter and your logged-in username.
If you have unread notifications, an accent badge like `[3]` appears beside the username.
If new topics arrive on the current list, a `New/updated (n)` indicator appears in the right side of the status bar.

Private Messages list view:
- Uses PM-specific columns in wide layouts.
Expand Down Expand Up @@ -246,7 +267,7 @@ In fullscreen image view, press `x` or `esc` to return to the topic.
- Press `n` from the topic list, topic view, or search results to open notifications.
- Arrow keys move through notifications; Enter opens the related topic/post.
- Press `f` to cycle notification filters (`All`, `Responses`, `Likes`, `Mentions`, `Edits`, `Links`, `Messages`).
- Opening a notification marks it read in termcourse.
- Opening a notification marks it read in termcourse after the server confirms the change.
- `esc` returns to the previous screen.

## Debug & Logging
Expand Down
130 changes: 130 additions & 0 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
de:
cli:
usage: "Verwendung: termcourse [optionen] <discourse_url>"
help:
api_key: "Discourse-API-Schlüssel (oder DISCOURSE_API_KEY)"
api_username: "Discourse-API-Benutzername (oder DISCOURSE_API_USERNAME)"
username: "Discourse-Benutzername/E-Mail (oder DISCOURSE_USERNAME)"
password: "Discourse-Passwort (oder DISCOURSE_PASSWORD)"
theme: "Theme-Name (überschreibt TERMCOURSE_THEME für diesen Lauf)"
lang: "Sprache der Oberfläche (en, fr, de oder es; Standard: System-/Umgebungs-Locale)"
show: "Hilfe anzeigen"
core_env: "Zentrale Umgebungsvariablen:"
env:
lang: "Sprache der Oberfläche: en|fr|de|es (Standard: TERMCOURSE_LANG, dann System-Locale, dann en)."
auth:
missing: "Authentifizierung fehlt. API-Schlüssel oder Benutzername/Passwort angeben."
api: "API-Schlüssel: DISCOURSE_API_KEY + DISCOURSE_API_USERNAME"
login: "Login: DISCOURSE_USERNAME + DISCOURSE_PASSWORD"
username: "Benutzername oder E-Mail:"
password: "Passwort:"
backup_code: "Backup-Code eingeben:"
two_factor: "2FA-Code eingeben:"
choose_2fa: "2FA-Methode wählen:"
totp: "TOTP (empfohlen)"
backup: "Backup-Code"
errors:
missing_url: "discourse_url fehlt."
login_failed: "Anmeldung fehlgeschlagen."
ui:
controls:
topic_list: "Pfeile: bewegen | ↵, 1-0: öffnen | c: neu | n: notifs | s: suchen | f: filter | g: aktualisieren | q: beenden"
topic_list_top: "Pfeile: bewegen | ↵, 1-0: öffnen | c: neu | n: notifs | s: suchen | f: filter | p: zeitraum | g: aktualisieren | q: beenden"
topic: "Pfeile: bewegen | l: liken | r: auf Thema antworten | p: auf Beitrag antworten | s: suchen | n: notifs | esc: zurück | q: beenden"
topic_with_image: "Pfeile: bewegen | l: liken | r: auf Thema antworten | p: auf Beitrag antworten | s: suchen | n: notifs | esc: zurück | q: beenden | x: bild"
fullscreen_image: "x/esc: zurück"
search_results: "Pfeile: bewegen | ↵: öffnen | n: notifs | esc: zurück | q: beenden"
notifications: "Pfeile: bewegen | ↵: öffnen | f: filter | esc: zurück | q: beenden"
category_picker: "Pfeile verwenden, Enter zum Auswählen, Esc zum Abbrechen"
composer: "Zeichen: %{status} | Pfeile: bewegen | Fertig: Ctrl+D | Neue Zeile: Enter | Abbrechen: Esc"
status:
topic_list: "Themenliste: %{filter}"
topic_list_with_period: "Themenliste: %{filter} (%{period})"
loading_more: "Lädt mehr..."
topic: "Thema: %{title}"
search: "Suche: %{query}"
notifications: "Benachrichtigungen: %{filter}"
logged_in: "Angemeldet: %{username}"
new_updated: "Neu/aktualisiert (%{count})"
pm_unread: "PN ungelesen (%{count})"
empty:
topics: "Keine Themen gefunden."
posts: "Keine Beiträge."
results: "Keine Ergebnisse."
notifications: "Keine Benachrichtigungen."
scroll:
more_above: "^^^ mehr oben ^^^"
more_below: "vvv mehr unten vvv"
posts:
image: "[bild]"
expand_image: "x: bild vergrößern"
composer:
reply_to_topic: "Auf Thema antworten"
reply_to_post: "Auf Beitrag #%{post_number} antworten (Ctrl+D zum Beenden)"
new_topic_title: "Neues Thema"
new_topic_body: "Neues Thema: %{title}"
select_category: "Kategorie auswählen"
enter_title: "Titel eingeben und Enter drücken"
title_prompt: "Titel: %{buffer}"
search_prompt: "Suche: %{buffer}"
no_category_option: "Keine Kategorie"
category_none: "Kategorie: keine"
category_named: "Kategorie: %{name}"
category_fallback: "Kategorie %{id}"
retry: "Beliebige Taste drücken, um es erneut zu versuchen..."
body_too_short: "Text zu kurz"
compose: "Verfassen"
replying_to: "Antwort an @%{username}:"
search:
title: "Suche"
prompt: "Suchbegriff eingeben und Enter drücken"
topic_fallback: "Thema %{id}"
notifications:
columns:
user: "Benutzer"
type: "Typ"
title: "Titel"
ago: "Vor"
unread_marker: "• "
filters:
all: "Alle"
responses: "Antworten"
likes: "Likes"
mentions: "Erwähnungen"
edits: "Bearbeitungen"
links: "Links"
messages: "Nachrichten"
topic_list:
columns:
index: "#"
title: "Titel"
category: "Kategorie"
replies: "Antworten"
views: "Aufrufe"
users: "Nutzer"
filters:
latest: "Neueste"
unread: "Ungelesen"
private: "Private Nachrichten"
hot: "Beliebt"
new: "Neu"
top: "Top"
periods:
daily: "Täglich"
weekly: "Wöchentlich"
monthly: "Monatlich"
quarterly: "Quartalsweise"
yearly: "Jährlich"
pm_users:
count: "%{count} Nutzer"
none: "-"
errors:
title: "Fehler"
continue: "Beliebige Taste drücken, um fortzufahren..."
time:
seconds: "%{count}s"
minutes: "%{count}m"
hours: "%{count}h"
days: "%{count}t"
weeks: "%{count}w"
years: "%{count}j"
130 changes: 130 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
en:
cli:
usage: "Usage: termcourse [options] <discourse_url>"
help:
api_key: "Discourse API key (or DISCOURSE_API_KEY)"
api_username: "Discourse API username (or DISCOURSE_API_USERNAME)"
username: "Discourse username/email (or DISCOURSE_USERNAME)"
password: "Discourse password (or DISCOURSE_PASSWORD)"
theme: "Theme name (overrides TERMCOURSE_THEME for this run)"
lang: "UI language (en, fr, de or es; default: env/system locale)"
show: "Show help"
core_env: "Core environment variables:"
env:
lang: "UI language: en|fr|de|es (default: TERMCOURSE_LANG, then system locale, then en)."
auth:
missing: "Missing auth. Provide API key or username/password."
api: "API key: DISCOURSE_API_KEY + DISCOURSE_API_USERNAME"
login: "Login: DISCOURSE_USERNAME + DISCOURSE_PASSWORD"
username: "Username or email:"
password: "Password:"
backup_code: "Enter backup code:"
two_factor: "Enter 2FA code:"
choose_2fa: "Choose 2FA method:"
totp: "TOTP (Recommended)"
backup: "Backup code"
errors:
missing_url: "Missing discourse_url."
login_failed: "Login failed."
ui:
controls:
topic_list: "arrows: move | ↵, 1-0: open | c: new | n: notifs | s: search | f: filter | g: refresh | q: quit"
topic_list_top: "arrows: move | ↵, 1-0: open | c: new | n: notifs | s: search | f: filter | p: period | g: refresh | q: quit"
topic: "arrows: move | l: like | r: reply topic | p: reply post | s: search | n: notifs | esc: back | q: quit"
topic_with_image: "arrows: move | l: like | r: reply topic | p: reply post | s: search | n: notifs | esc: back | q: quit | x: image"
fullscreen_image: "x/esc: back"
search_results: "arrows: move | ↵: open | n: notifs | esc: back | q: quit"
notifications: "arrows: move | ↵: open | f: filter | esc: back | q: quit"
category_picker: "Use arrows, Enter to select, Esc to cancel"
composer: "Chars: %{status} | Arrows: move | Finish: Ctrl+D | New line: Enter | Cancel: Esc"
status:
topic_list: "Topic List: %{filter}"
topic_list_with_period: "Topic List: %{filter} (%{period})"
loading_more: "Loading more..."
topic: "Topic: %{title}"
search: "Search: %{query}"
notifications: "Notifications: %{filter}"
logged_in: "Logged in: %{username}"
new_updated: "New/updated (%{count})"
pm_unread: "PM Unread (%{count})"
empty:
topics: "No topics found."
posts: "No posts."
results: "No results."
notifications: "No notifications."
scroll:
more_above: "^^^ more above ^^^"
more_below: "vvv more below vvv"
posts:
image: "[image]"
expand_image: "x: expand image"
composer:
reply_to_topic: "Reply to topic"
reply_to_post: "Reply to post #%{post_number} (Ctrl+D to finish)"
new_topic_title: "New Topic"
new_topic_body: "New Topic: %{title}"
select_category: "Select Category"
enter_title: "Enter title and press Enter"
title_prompt: "Title: %{buffer}"
search_prompt: "Search: %{buffer}"
no_category_option: "No category"
category_none: "Category: none"
category_named: "Category: %{name}"
category_fallback: "Category %{id}"
retry: "Press any key to try again..."
body_too_short: "Body too short"
compose: "Compose"
replying_to: "Replying to @%{username}:"
search:
title: "Search"
prompt: "Type query and press Enter"
topic_fallback: "Topic %{id}"
notifications:
columns:
user: "User"
type: "Type"
title: "Title"
ago: "Ago"
unread_marker: "• "
filters:
all: "All"
responses: "Responses"
likes: "Likes"
mentions: "Mentions"
edits: "Edits"
links: "Links"
messages: "Messages"
topic_list:
columns:
index: "#"
title: "Title"
category: "Category"
replies: "Replies"
views: "Views"
users: "Users"
filters:
latest: "Latest"
unread: "Unread"
private: "Private Messages"
hot: "Hot"
new: "New"
top: "Top"
periods:
daily: "Daily"
weekly: "Weekly"
monthly: "Monthly"
quarterly: "Quarterly"
yearly: "Yearly"
pm_users:
count: "%{count} users"
none: "-"
errors:
title: "Error"
continue: "Press any key to continue..."
time:
seconds: "%{count}s"
minutes: "%{count}m"
hours: "%{count}h"
days: "%{count}d"
weeks: "%{count}w"
years: "%{count}y"
Loading
Loading