feat: add TUI client with FIFO communication and status display
This commit is contained in:
committed by
Loic Coenen (aider)
parent
5341cb676a
commit
971372eac9
148
docs/8-tui.md
Normal file
148
docs/8-tui.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# TUI Client – Architecture and Usage
|
||||
|
||||
## Overview
|
||||
|
||||
The TUI client (`looper-client`) is a standalone ncurses application that communicates with the looper engine **only** via two named pipes:
|
||||
|
||||
- `/tmp/looper_cmd` – the client writes text commands to the engine.
|
||||
- `/tmp/looper_status` – the engine writes one line per active channel after each main‑loop iteration, reporting the current scene state.
|
||||
|
||||
The client never links against engine source code. It is built from files in `client/src/` and linked only with `-lncurses`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User keypress
|
||||
│
|
||||
▼
|
||||
tui_run() ──► getch() ──► switch(ch)
|
||||
│ │
|
||||
│ ▼
|
||||
│ send_command(cmd)
|
||||
│ │
|
||||
│ ▼
|
||||
│ write("/tmp/looper_cmd")
|
||||
│
|
||||
│ ┌──────────────────┐
|
||||
│ │ Engine main loop │
|
||||
│ │ (looper.c) │
|
||||
│ │ │
|
||||
│ │ looper_process_ │
|
||||
│ │ commands() │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ looper_write_ │
|
||||
│ │ status() │
|
||||
│ │ │ │
|
||||
│ └─────────┼────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ write("/tmp/looper_status")
|
||||
│
|
||||
│ read("/tmp/looper_status") ◄──────────── (non‑blocking open)
|
||||
│ │
|
||||
│ ▼
|
||||
▼
|
||||
parse_status_line(...)
|
||||
│
|
||||
▼
|
||||
cell_state[ch] = state
|
||||
│
|
||||
▼
|
||||
draw_grid() ──► state_to_color(state) returns colour pair
|
||||
apply colour to cell
|
||||
```
|
||||
|
||||
## Key Bindings
|
||||
|
||||
| Key | Action | FIFO command sent |
|
||||
|------------------|---------------------------------------------|------------------------------|
|
||||
| `h` / `←` | Move selection left | (none) |
|
||||
| `j` / `↓` | Move selection down | (none) |
|
||||
| `k` / `↑` | Move selection up | (none) |
|
||||
| `l` / `→` | Move selection right | (none) |
|
||||
| `t` | Record / toggle on selected column | `record <col>\n` |
|
||||
| `s` | Next scene | `scene_next\n` |
|
||||
| `S` | Previous scene | `scene_prev\n` |
|
||||
| `d` / `D` | Stop all channels | `stop\n` |
|
||||
| `a` | Add audio channel | `add\n` |
|
||||
| `A` | Add MIDI channel | `add_midi\n` |
|
||||
| `r` | Remove last dynamic channel | `remove\n` |
|
||||
| `b` | Bind to selected column | `bind <col>\n` |
|
||||
| `u` | Unbind (reset to channel 0) | `unbind\n` |
|
||||
| `?` | Toggle help text | (none) |
|
||||
| `Esc` / `Q` | Quit | (none) |
|
||||
|
||||
## Status Line Format
|
||||
|
||||
Each line written by the engine to `/tmp/looper_status` follows this pattern:
|
||||
|
||||
```
|
||||
CH=<channel_number> SC=<scene_index> STATE=<state_string>
|
||||
```
|
||||
|
||||
`<state_string>` is one of `IDLE`, `RECORD`, `LOOPING`, `PAUSED`.
|
||||
|
||||
Example:
|
||||
```
|
||||
CH=0 SC=0 STATE=RECORD
|
||||
CH=1 SC=0 STATE=LOOPING
|
||||
```
|
||||
|
||||
The client parses these lines and updates the colour of the corresponding cell:
|
||||
|
||||
- `IDLE` → white (`COLOR_EMPTY`)
|
||||
- `RECORD` → red (`COLOR_RECORDING`)
|
||||
- `LOOPING` → green (`COLOR_LOOPING`)
|
||||
- `PAUSED` → blue (`COLOR_STOPPED`)
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Engine
|
||||
|
||||
```sh
|
||||
cd engine
|
||||
make # produces `looper`
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```sh
|
||||
cd client
|
||||
make # produces `looper-client`
|
||||
```
|
||||
|
||||
### Running Together
|
||||
|
||||
1. Start the JACK server (e.g., `jackd -d alsa` or `pipewire`).
|
||||
2. In a terminal, start the engine:
|
||||
```sh
|
||||
cd engine && ./looper
|
||||
```
|
||||
3. In another terminal, start the client:
|
||||
```sh
|
||||
cd client && ./looper-client
|
||||
```
|
||||
4. Use the TUI keys described above.
|
||||
|
||||
## Cleanup
|
||||
|
||||
When the client exits, it deletes both FIFOs (`/tmp/looper_cmd` and `/tmp/looper_status`).
|
||||
If the engine is still running, it will continue to try to write to the status FIFO; that write will fail silently (the engine uses `O_NONBLOCK` and ignores errors).
|
||||
The engine creates the status FIFO on startup and does not delete it.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit test for status line parser**: `make test` in `client/` runs `test_status_parse`.
|
||||
- **Integration test for status FIFO** (engine side): `make test` in `engine/tests/` runs `test_status_fifo`.
|
||||
|
||||
These are **not** executed automatically from the top‑level `make test` – they must be invoked manually or added to the top‑level Makefile.
|
||||
|
||||
The engine status FIFO test (`test_status_fifo`) uses `select()` with a timeout and retry loop to wait for a status line showing `STATE=RECORD`. It is reliable and does not hang.
|
||||
|
||||
## Future Work
|
||||
|
||||
- Replace dead stubs (`FuzzySearch`, `marks`, `yank_buffer`, visual mode) with real implementations or remove them.
|
||||
- Support transport play/pause via a dedicated FIFO command.
|
||||
- Allow the client to display multiple scenes per channel (e.g., via a tab or side panel).
|
||||
- Graceful error recovery when the engine or FIFO is not available.
|
||||
Reference in New Issue
Block a user