From 5ad831f50c63b0f26d5cacdb1903e4f86c009e6f Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Wed, 13 May 2026 19:27:07 +0000 Subject: [PATCH] docs: add plan for refactoring TUI into standalone FIFO-client binary --- client/PLAN.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 client/PLAN.md diff --git a/client/PLAN.md b/client/PLAN.md new file mode 100644 index 0000000..b169a8d --- /dev/null +++ b/client/PLAN.md @@ -0,0 +1,136 @@ +# Plan: Refactor TUI into Standalone FIFO‑Client Binary + +## Goal +Extract the TUI from the existing monolithic codebase into a separate `looper-client` binary that communicates with the engine **only** via the FIFO pipe (`/tmp/looper_cmd`). +The TUI must **not** link to any engine source files (`engine.c`, `dispatcher.c`, `carla.c`, etc.) or use their headers beyond shared type definitions (e.g., `command.h`). All state is maintained by the engine; the TUI sends commands and assumes they succeed. + +## Background +- The looper engine runs as a separate JACK client and listens for commands on: + - FIFO pipe (`/tmp/looper_cmd`) – text‑based commands + - `looper:control` – MIDI note‑on events +- The current TUI uses a local `Engine` / `AppState` / `dispatcher` that does **not** talk to the real looper. It was designed for unit testing. + +## Task 1 – Create a new `client/` directory structure +- Keep existing `client/src/tui.c` and `client/src/tui.h`. +- Remove all `#include` directives that reference engine internal headers: + - `engine.h` + - `dispatcher.h` + - `wav_io.h` + - `transport.h` + - `carla.h` +- Remove any `Engine*`, `AppState`, `DispatchFn`, `Clip`, `MidiClip` usage. +- **Keep** the `FuzzySearch` struct, `draw_rack_view()`, `handle_rack_view()`, `list_wav_files()`, `load_sample_callback`, mouse handling, etc. – they **are not removed**. + However, every call to engine internals (e.g., `carla_get_available_plugins`, `dispatcher_get_state`, `g_dispatch`, `carla.h` functions) **must be replaced** with a stub that does nothing (or prints a debug message) until the engine implements the corresponding FIFO commands. + This keeps the UI code compilable and preserves the structure for future implementation. + +## Task 2 – Implement `send_command()` and open FIFO +- Add a function: + + ```c + int send_command(const char *cmd_line); + ``` + + This function: + - Opens `/tmp/looper_cmd` with `O_WRONLY`. + - Writes the command string (e.g., `"record 0\n"`). + - Appends a newline if missing. + - Closes the file descriptor. + - Returns 0 on success, -1 on error (prints diagnostic to stderr). + +- The TUI should call `send_command()` for every user action that should affect the engine. + +## Task 3 – Map TUI keys to FIFO commands +Replace each `g_dispatch(action)` with a direct FIFO command string. + +**Proposed mapping (simplified – can be refined later):** + +| TUI key/action | FIFO command | +|--------------------------------------------|-----------------------------------------------------| +| `'t'` (trigger clip at selected cell) | `record \n` (channel = `selected_col`) | +| `'d'` (reset clip) | `stop\n` (global stop) | +| `'s'` (trigger scene – current row) | `"scene_next\n"` (or `"scene_add\n"`? TBD) | +| `' '` (toggle transport play/pause) | No corresponding FIFO command yet. Omit for now. | +| `'S'` (stop transport) | `"stop\n"` | +| `'q'` (cycle quantize) | No FIFO equivalent – ignore. | +| `'x'` (reset transport) | `"stop\n"` | +| `'N'` (play next scene) | `"scene_next\n"` | +| `'P'` (play previous scene) | `"scene_prev\n"` | +| `'u'` (undo) | No FIFO equivalent – ignore. | +| `Ctrl+R` (redo) | Ignore. | +| `'v'`, `'V'` (visual mode) | Keep visual selection logic but send commands only for `'d'` and `'y'` actions. | +| `'y'` (yank) | Do nothing (local clipboard only). | +| `'p'` (paste) | For each pasted cell send `"record \n"`. | +| `'m'` (move mode) | No effect on engine – local navigation. | +| `'z'` (zoom grid selector) | Local navigation only. | +| `'G'` (toggle audio/MIDI grid) | No FIFO command – ignore. | +| `'-'` / `'='` (volume) | No FIFO command – ignore. | +| `\t` (switch to rack view) | Remove entirely. | +| `':'` command mode | Keep for `:q` (quit) and `:rack` commands (the latter can be removed). | +| Escape / `'Q'` | Quit the TUI (no command sent). | + +**Channel binding:** +- When the user moves selection to a new column, send `"bind \n"` to ensure subsequent commands affect the correct channel. This can be done in the navigation switch cases. + +## Task 4 – Remove all references to `Engine` and `dispatcher` +- Delete the lines: + - `static Engine *g_engine = NULL;` + - `static DispatchFn g_dispatch = NULL;` +- Replace calls like `g_dispatch(action)` with `send_command(formatted_string)`. +- Remove `dispatcher_get_state()` calls – the TUI will no longer query the engine state. Update `draw_cell()` to display only static info (clip index) or a fixed colour (e.g., all green). The state‑dependent colouring is not available without feedback from the engine. For now, show all cells as idle (white) or use a placeholder. +- Remove the line `AppState state; dispatcher_get_state(&state);` inside draw functions. + +## Task 5 – Simplify the `tui.h` header +- Replace the function signatures: + + ```c + void tui_init(void); // no Engine* argument + void tui_run(void); // no Engine* argument + void tui_cleanup(void); + ``` + +- Remove `#include "engine.h"` and `#include "dispatcher.h"`. +- Remove the `Engine*` parameter from the init and run functions. + +## Task 6 – Create `client/main.c` +- Write a simple `main()` that: + - Optionally opens the FIFO for writing just to check it exists. + - Calls `tui_init()`. + - Calls `tui_run()`. + - Calls `tui_cleanup()`. + - Returns 0. + +## Task 7 – Write `client/makefile` +- Target `looper-client`: + - Compile `src/tui.c` and `src/main.c` (or `src/client.c` if split). + - **Do not** link to any engine `.o` files. + - Link only with `-lncurses` (and `-lm` if needed). + - Example: + ```makefile + CC ?= gcc + CFLAGS ?= -Wall -Wextra -g -I../engine/src + LDFLAGS ?= -lncurses -lm + + looper-client: src/tui.c src/main.c + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + ``` + +## Task 8 – Remove dead code and unnecessary helpers +- Delete `utils.c` / `utils.h` if they existed only for TUI – but keep all WAV‑related, rack‑related, and fuzzy‑search code (they are now stubs). +- Remove `clip_state_to_string()`, `transport_state_to_string()`, `quantize_mode_to_string()`, `clock_source_to_string()` – they are no longer used for display because we have no `AppState`. + Replace them with static strings that show placeholder text (e.g., `"N/A"`). +- Remove `state_to_color()` – instead use a fixed colour pair (e.g., all cells white) or remove colour entirely, because we have no clip state. +- Remove the `mouse` callback if it relied on `dispatcher_get_state` – but keep the function body as a no‑op. + +## Task 9 – Test the new client +- Build `looper-client` and verify it compiles without engine object files. +- Start the engine (`./looper` in `engine/`). +- Run `./looper-client` and press keys that should generate FIFO commands. Use `cat /tmp/looper_cmd` in another terminal to verify output. +- Check that commands like `record 2`, `stop`, `bind 3` appear. + +## Notes / Future Improvements +- **State feedback:** The TUI currently shows clip state colours. To restore that, a separate FIFO (or shared memory) for engine‑>client status could be added. Not part of this plan. +- **MIDI grid / rack view:** These depend on engine features not yet exposed via FIFO. They are removed; can be re‑added later. +- **Transport commands:** The engine does not have a dedicated transport play/pause command via FIFO; it relies on MIDI notes. Future FIFO extension needed. + +This plan produces a clean, minimal client that interfaces only through the named pipe. +````