Metadata-Version: 2.4
Name: tyrtulia-tui
Version: 1.6.0
Summary: Terminal client for Tyrtulia, the headless forum for small multi-user servers
Keywords: forum,bbs,tilde,pubnix,tui,textual
Author: Andros Fenollosa
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Requires-Dist: httpx>=0.28
Requires-Dist: textual>=8.2
Requires-Python: >=3.14
Project-URL: Homepage, https://git.andros.dev/andros/tyrtulia-tui
Project-URL: Repository, https://git.andros.dev/andros/tyrtulia-tui
Description-Content-Type: text/markdown

# Tyrtulia TUI

Terminal client for [Tyrtulia](https://git.andros.dev/andros/tyrtulia), the headless forum for small multi-user servers (tildes, pubnixes, club servers). Built with [Textual](https://textual.textualize.io).

The server, its API and how to run an instance are in the [Tyrtulia repository](https://git.andros.dev/andros/tyrtulia) (also on PyPI as [`tyrtulia`](https://pypi.org/project/tyrtulia/)).

- On the server itself it talks to Tyrtulia through its Unix socket: the operating system tells the forum who you are, so there is nothing to log in to.
- From anywhere else it logs in with your SSH key (a signed challenge) and keeps the session token in a private file. Before signing, it checks that the login message names the server it is talking to, so a malicious server cannot reuse your signature on another instance.
- It only knows `GET /` and follows the links the forum returns: an action appears in the footer only when the forum offers it to you (reply, edit, pin, hide...). It never builds, reads or changes a URL of the API: page sizes and positions are variables of the forum's templated links, and orders, views and filters are links of their own.
- Notifications arrive in real time over Server-Sent Events.
- It reads several forums at once: their threads and notifications come together in one list, and search results side by side. You write only where you are registered (where your SSH key is, or through the socket); elsewhere you read as a guest.

![A thread in Tyrtulia TUI](https://git.andros.dev/andros/tyrtulia-tui/raw/branch/main/docs/thread.png)

One forum:

![The list of threads of one forum](https://git.andros.dev/andros/tyrtulia-tui/raw/branch/main/docs/threads.png)

Three forums at once, the last one read as a guest:

![The threads of three forums in one list](https://git.andros.dev/andros/tyrtulia-tui/raw/branch/main/docs/forums.png)

## Requirements

- Python 3.14.
- OpenSSH client (`ssh-keygen`), to sign the login challenge when you connect from outside the server.
- A [Tyrtulia](https://git.andros.dev/andros/tyrtulia) instance, 1.5 or later for everything. Older forums still work, with less: no tree or levels view (threads show in order), no order by activity, no unread-only notifications and no search in titles; the client never rebuilds those by hand. 1.2 adds login messages bound to the instance. To set one up, follow its [getting started guide for administrators](https://git.andros.dev/andros/tyrtulia#getting-started-for-administrators).

## Installation

With [uv](https://docs.astral.sh/uv/):

```sh
uv tool install tyrtulia-tui
```

With pip:

```sh
python3.14 -m venv ~/.local/share/tyrtulia-tui
~/.local/share/tyrtulia-tui/bin/pip install tyrtulia-tui
ln -s ~/.local/share/tyrtulia-tui/bin/tyrtulia-tui ~/.local/bin/tyrtulia-tui
```

Administrators can install it once for every user of the server, for example in `/opt/tyrtulia-tui` with a link in `/usr/local/bin`.

## Usage

On the server, just run it. It finds the socket in `/run/tyrtulia/tyrtulia.sock` (or `/var/run/tyrtulia/tyrtulia.sock` on BSD):

```sh
tyrtulia-tui
```

From another machine, point it to the forum and give your username and SSH key. Your first SSH key is registered from the server itself (profile, `a`) or by the operator:

```sh
tyrtulia-tui --url https://forum.example.org --user alice --key ~/.ssh/id_ed25519
```

The key can also be the `.pub` file, to sign with `ssh-agent`. Without `--user`, you read as a guest and can log in later with `l`.

### Several forums

Repeat `--url` (or `--socket`) to read several forums together:

```sh
tyrtulia-tui --url https://forum.example.org --url https://tilde.example --user alice
```

- The list of threads mixes every forum into one, with a column that says where each one comes from: pinned threads first, then the newest by creation (the default) or by the last post (`s`). Each forum keeps its own order, and the next thread is always the newest of what every forum has left, asking a forum for its next page only when it runs out; so the order holds across pages, even when one forum is much busier than another. Forums older than 1.5 do not order by activity and are left out of that order, with a notice. Notifications are mixed the same way, newest first.
- Search results are interleaved: the best match of each forum, then the second of each, and so on, since each forum scores against its own posts and the scores do not compare. The screen says so.
- At start it logs in to every forum with the same username and key. Where your key is not registered, the login fails and you read that forum as a guest: no reply, answer or new thread there. `l` logs in to one of them later.
- When an action needs one forum (`n` new thread, `p` profile, `@` user, `l` log in, `o` log out) and more than one fits, it asks which. `n` only offers the forums where you can write.
- A forum that cannot be reached at start is left out, with a message.

| Option | Meaning |
|---|---|
| `--socket PATH` | Unix socket of a server. Repeatable. |
| `--url URL` | URL of a server, for remote access. Repeatable. |
| `--user NAME` | Username for the SSH key login. |
| `--key FILE` | SSH private key (or its `.pub` with the agent). Defaults to `~/.ssh/id_ed25519`, `id_ecdsa` or `id_rsa`. |
| `--editor COMMAND` | Your editor for `Ctrl+O` (see [Writing in your own editor](#writing-in-your-own-editor)). Defaults to `$VISUAL`, `$EDITOR` or `vi`. |
| `--config FILE` | Configuration file. |

### Configuration

Every key is optional; command line options win:

```toml
# ~/.config/tyrtulia-tui/config.toml
url = "https://forum.example.org"
username = "alice"
key = "~/.ssh/id_ed25519"
editor = "nvim"            # for Ctrl+O; also "emacs -nw", "nano", "code --wait"...
# socket = "/run/tyrtulia/tyrtulia.sock"
```

For several forums, add a `[[forum]]` table for each one. The top-level `username` and `key` apply to every forum that does not set its own:

```toml
username = "alice"
key = "~/.ssh/id_ed25519"

[[forum]]
url = "https://forum.example.org"

[[forum]]
url = "https://tilde.example"
username = "alice_t"       # another name on this one

[[forum]]
socket = "/run/tyrtulia/tyrtulia.sock"
```

Without `--url` or `--socket` on the command line, it opens the forums of the configuration and, if there is one, the local socket of the server you are on. `--url` and `--socket` replace them all.

Session tokens are kept in `~/.local/state/tyrtulia-tui/tokens.json` (mode `0600`), and the last post you read in each thread in `read.json` next to it, to show how many posts are new. Both follow `XDG_STATE_HOME`.

## Keys

| Where | Key | Action |
|---|---|---|
| Everywhere | `q` | Quit |
| | `i` | Notifications |
| | `/` | Search (`words`, `"a phrase"`, `OR`, `NOT`, `prefix*`) |
| | `p` | Your profile and SSH keys |
| | `@` | Open a user by name |
| | `l` / `o` | Log in / log out (remote access) |
| Lists | `↑` `↓` (`k` `j`) | Move up and down (threads, notifications, search results, SSH keys) |
| Threads | `Enter` | Open |
| | `n` | New thread (asks in which forum, if you can write in several) |
| | `w` | Latest or watching |
| | `s` | Order: by creation (default) or by the last post |
| | `r` | Refresh |
| | `]` / `[` | Next / previous page |
| Thread | `↑` `↓` (`k` `j`) | Move between posts (the selected one has an orange bar) |
| | `u` | Go to the post the selected one answers; in the levels view, from the post on top, go up one level |
| | `w` | Open the profile of the selected post's author |
| | `v` | [View](#thread-views): tree, levels or linear |
| | `Enter` | Levels view: open the selected post with its answers (or click `▸ N replies`) |
| | `r` | Reply to the thread |
| | `a` | Answer the selected post |
| | `e` / `d` | Edit / delete the selected post |
| | `s` | Subscription: watching, normal, muted |
| | `t` | Change the title |
| | `f` | Report the selected post |
| | `h`, `P`, `C` | Moderators: hide or unhide, pin, close |
| | `Esc` | Back |
| Editor | `Ctrl+S` | Send |
| | `Ctrl+O` | Continue in your own editor |
| | `F7` | Select all |
| | `Esc` | Cancel |
| Notifications | `Enter` | Open what it points to: the post, or for reports the post, thread or user |
| | `a` | Mark all as read |
| | `u` | Only unread, or all |
| Search | `Ctrl+T` | Only in titles, or in titles and posts |
| Profile | `e` | Edit your about |
| | `a` / `x` | Add an SSH key from a `.pub` file / delete the selected key |
| User | `f` | Report the user |
| | `B`, `R` | Moderators: ban or unban, make read-only or remove it (a reason is asked and is public in the moderation log) |
| | `Esc` | Back |

Only the actions the forum allows you appear in the footer.

### Thread views

`v` cycles through three ways of reading a thread; the title shows the current one.

- **Tree**: the whole conversation, each answer indented under the post it answers.
- **Levels**: one level at a time, for long threads such as wiki pages. It starts with the posts that answer no other post. `Enter` (or a click on `▸ N replies`) opens a post with its direct answers, and so on; `u` on the post on top goes back up. The title shows where you are, such as `levels: #2 > #3`. Only the level on screen is fetched, so it works on threads of any size.
- **Linear**: every post in the order it was written.

Tree and levels need Tyrtulia 1.5: on older forums the thread shows in order, and the title says why. A thread shows up to 500 posts; `m` asks for more.

### Writing in your own editor

You can write posts in your usual terminal editor instead of the built-in one:

1. Open the editor of the TUI as usual: `n` for a new thread, `r` to reply, `a` to answer a post or `e` to edit one. For a new thread, type the title first.
2. Press `Ctrl+O`. The TUI steps aside and your editor opens with the draft.
3. Write, save and quit (`:wq` in vim, `C-x C-s` and `C-x C-c` in Emacs, `C-x C-s` and `C-x #` with `emacsclient`, `Ctrl+O` and `Ctrl+X` in nano).
4. You are back in the TUI with the text. Check it and press `Ctrl+S` to send, or `Ctrl+O` again to keep editing.

The editor is, in this order: `--editor` on the command line, `editor` in the configuration, `$VISUAL`, `$EDITOR` and finally `vi`. Arguments are allowed:

```toml
# ~/.config/tyrtulia-tui/config.toml
editor = "nvim"
# editor = "emacs -nw"
# editor = "emacsclient"     # already inside Emacs (vterm, eat, shell): finish with C-x #
# editor = "nano"
# editor = "hx"
# editor = "code --wait"     # graphical editors need their "wait" option
```

```sh
tyrtulia-tui --editor "emacs -nw"
```

- The draft is a temporary `.md` file only you can read, removed when you come back.
- If the editor exits with an error or cannot be found, the text in the TUI stays as it was and the reason is shown.
- The rules of the forum (lengths, too many mentions...) are checked when you send, as with the built-in editor; if the forum rejects the post, the editor opens again with your text.
- Notifications keep arriving while you write; you see them when you come back.

## Development

Everything runs in Docker:

```sh
docker compose run --rm dev sh -c 'ruff format src tests && ruff check src tests && mypy && pytest'
```

The tests drive the client and the interface (Textual's `Pilot`) against a real Tyrtulia instance running in-process, plus a real `tyrtulia serve` with Unix users inside the container. The server is the one locked from PyPI (1.4 for now, which exercises the degraded paths; tests that need 1.5 are skipped); to test 1.5 before it is published, install its wheel over it:

```sh
docker compose run --rm -v /path/to/wheels:/wheels dev sh -c 'uv pip install --python /opt/venv/bin/python /wheels/tyrtulia-*.whl && pytest'
```

`tests/test_no_paths.py` fails if the client writes a path of the API or a query parameter by hand: the contract is the link relations and the representations, so URLs are never built, read or changed.

`e2e/run.sh` runs the end-to-end test: on Debian with systemd, ten people use the real TUI in `tmux` against one Tyrtulia 1.5 instance installed from PyPI (until 1.5.0 is published, pass a wheel: `TYRTULIA_WHEEL=/path/to/tyrtulia-1.5.0-py3-none-any.whl e2e/run.sh`). Five Unix users go through the socket and five SSH key accounts log in over HTTP; they post, answer, mention, edit, moderate, report and search, and see each other's notifications in real time.

## Contributing

Contributions are welcome! Please see the [contribution guidelines](https://git.andros.dev/andros/contribute) for instructions on how to submit issues or pull requests. Changes to the forum itself (the API, the server) belong in the [Tyrtulia repository](https://git.andros.dev/andros/tyrtulia).

## License

Tyrtulia TUI is free software, released under the [GNU General Public License v3.0 or later](LICENSE).
