A terminal client for jtech forums — and any other Discourse-powered site (but heavily customized for jtech) — built with Textual. Browse feeds, read threads, reply, react, search, check notifications, send private messages, follow the gamification leaderboard, and much more — all without leaving your shell.
Note on provenance. A large portion of this project was written by an AI coding assistant working against the public Discourse API. Expect rough edges and bugs. Please file issues with reproduction steps and I'll fix what I can.
- Full feed coverage — Latest, New, Top, Unseen, Categories, Messages, Notifications and Search, each as its own tab.
- Notification badge — unread notification count appears as a
● Npip on the Notifications tab label. - Jump between unread —
n/pon a feed walks forward/backward through topics with aneworunreadstate, in the list's display order. - Background prefetch on hover — moving the cursor over a feed row starts a
300 ms debounced thread fetch in the background so
enteris instant. The cache caps at 10 threads and never blocks the foreground feed. - Proper thread view — markdown rendering with syntax-highlighted code
blocks,
[quote=…]blocks converted to real blockquotes, per-post reaction summaries, a#post_numberpill, and a↱ replying to @user #Nbreadcrumb. - Resume-at-last-read — a topic you've opened before lands you where you stopped (first unread if there is one, otherwise the last post you saw), matching the web UI. Threads you've never opened start at the top.
- Open at the right post from notifications — tapping a notification opens the thread centered on the linked reply, not the first post.
- Lazy-loaded posts — long threads only render what you're reading; more posts stream in above/below as you scroll. Posts above the landing position start loading in the background the moment the thread opens, so scrolling back is seamless. Scroll position stays pinned on the post you're reading while new content mounts.
- Auto-collapse for long posts — posts over 40 lines fold into a short
preview;
xtoggles. - Compose in
$EDITORas.md— new topics, replies, quote-replies and PMs all drop you into your editor with proper markdown syntax highlighting. - Reactions — picker is hardcoded to the five reactions jtech forums
accepts (
ok_hand,man_shrugging,+1,folded_hands,laughing). Counts update in place with no reload.lopens a per-reaction panel showing who reacted with what. - Leaderboard — the gamification-plugin boards at
/leaderboard/<id>. Period switches with[/](daily, weekly, monthly, quarterly, yearly, all); cycle between boards with</>. - Copy menu —
Yon a post picks between the post permalink and any code blocks in the post; no code blocks → link copied directly. - Open in browser —
oon a post hands off totermux-open-url/xdg-open/open/webbrowser, opening on the exact post. - Live refresh — threads auto-poll every 30 s by default (
atoggles). The poll targets the tail of the loaded range so a dead thread won't spam old posts into the view. Feeds refresh automatically when you back out of a thread. - Upload attachments —
Uuploads a file via/uploads.jsonand copies the resulting markdown to your clipboard. - Remember me — optional at login. Stores the password in plaintext in the config so the client can silently re-authenticate when the session cookie expires.
- Silent reauth — when a session expires mid-session, the client refreshes the cookie in the background (if remember-me is on) without kicking you to the login screen.
- Vim-style navigation —
j/k/g/Geverywhere.↑at the top of a list hands focus to the tab bar without teleporting.
- Python 3.10+
- A Discourse forum with a username + password login (the client stores the
_tsession cookie) - A clipboard helper on
PATHfor yank / copy-link / copy-code:termux-clipboard-set,pbcopy,wl-copy,xclip, orxsel $EDITOR(or$VISUAL) set for compose — falls back tonano
git clone https://github.com/flipphoneguy/jtech-tui
cd jtech-tui
pip install -e .jtech # opens your default feed
jtech --feed latest # override the starting tabValid --feed values: latest, new, top, unseen, categories,
messages, notifications.
On first launch you'll be prompted for your forum URL, username and password.
Tick "Remember me" if you want the client to re-login automatically after a
cookie expiry. The session cookie (and optionally the password) is written to
~/.config/jtech-tui/config.json with mode 0600. Subsequent launches skip
the login screen.
~/.config/jtech-tui/config.json:
| Field | Default | Purpose |
|---|---|---|
forum_url |
https://forums.jtechforums.org |
Base URL of the Discourse site. |
default_feed |
latest |
Tab to open when --feed is not passed. |
session_cookie |
(empty) | Discourse _t cookie. Cleared on reauth. |
username |
(empty) | Cached for ownership checks on edit/delete. |
password |
(empty) | Plaintext; only written if "Remember me" is on. |
Point the client at a different Discourse host by editing forum_url and
clearing session_cookie so you get a fresh login prompt.
| Key | Action |
|---|---|
tab / shift+tab, ← / → |
Switch tabs |
↓ on a tab |
Focus the current list |
↑ at the top of a list |
Back to the tab bar |
j / k |
Move cursor down / up |
g / G |
Jump to top / bottom of the list |
n / p |
Next / previous unread topic in this feed |
enter |
Open the selected row |
/ |
Open the search modal |
N |
New topic |
M |
New private message |
U |
Upload a file |
L |
Open the leaderboard |
R |
Reload the current tab |
ctrl+q |
Quit |
? |
Show the full key list for this screen |
Reaching the bottom of a feed triggers a page fetch, so you get infinite-scroll
without a separate action. Moving the cursor over a row kicks off a background
prefetch of that thread so the next enter is instant.
| Key | Action |
|---|---|
j / k |
Next / previous post (scrolls within the post first if long) |
g / G |
Jump to the first / last post in the full topic |
enter |
Open the reaction picker on the highlighted post |
r |
Reply (threaded under the highlighted post if any) |
t |
Reply to the topic (never threaded) |
Q |
Quote-reply — opens $EDITOR with a [quote=…] prefilled |
y |
Copy the highlighted post's raw markdown |
Y |
Copy menu — pick between post link and any code blocks |
o |
Open the highlighted post in your browser |
p |
Jump to the post this one is replying to |
l |
Show who reacted to the highlighted post (per reaction) |
x |
Toggle collapse on the highlighted post |
u |
Open the author's profile |
+ / ctrl+r |
React to the highlighted post |
E |
Edit your own post (opens $EDITOR) |
D |
Delete your own post (with confirmation) |
a |
Toggle auto-refresh (on by default, polls every 30 s) |
e |
Open the full thread in $EDITOR as read-only .md |
U |
Upload a file and get its markdown on the clipboard |
R |
Hard-reload the thread |
esc / q |
Back to the previous screen |
| Key | Action |
|---|---|
[ / ] |
Previous / next period |
< / > |
Previous / next leaderboard (when > 1) |
enter |
Open the selected user's profile |
R |
Reload |
esc / q |
Back |
jtech_tui/
├── api.py # Thin Discourse client (requests.Session, CSRF, JSON)
├── app.py # Textual App + argparse entrypoint + silent reauth
├── config.py # ~/.config/jtech-tui/config.json (dataclass)
├── editor.py # $EDITOR round-trip with a temporary .md file
├── styles.tcss # Textual CSS
└── screens/
├── login.py # Login modal (with Remember me)
├── main.py # Tabbed top-level (feeds, categories, PMs, …)
├── thread.py # Thread view, lazy posts, reactions, composer
├── composer.py # Shared modal dialogs + hardcoded reaction list
├── leaderboard.py # Gamification plugin viewer
├── user_profile.py # User bio + recent activity
└── smart_footer.py # Footer that truncates to "? all keys"
All network calls run in textual.work threads so the UI never blocks; results
are marshalled back with call_from_thread.
- Gamification plugin required for the leaderboard feature. Without it the screen shows "Load failed".
- discourse-reactions plugin required for reactions. Posts on sites without it will return an error when you try to react.
- Images render as placeholder markdown, not actual pixels — terminals generally can't display images without extra setup, so this is a deliberate omission.
- Drafts are not persisted: quitting
$EDITORwithout writing discards the buffer. - Remember me is plaintext. The password is stored unencrypted in
config.json. The file is0600but anyone with read access to your home dir can read it — enable this only on trusted machines. - Infinite scroll is per-tab and resets on resume (so read state stays accurate after you come back from a thread).
Issues and pull requests welcome. If you're filing a bug, please include:
- The forum URL (if it's a public site)
- Output of
python --versionandpip show textual - The traceback, if any, and the steps to reproduce
Run the app from source with pip install -e . then jtech. There are no
tests yet; adding some is a welcome contribution.
GPL-3 See LICENSE