Metadata-Version: 2.1
Name: micronetcode
Version: 0.3.1
Summary: micronetcode + ui_cli_manager: generic TCP command channel + CLI-native integration for UI apps
Home-page: https://gitlab.com/meehai/micronetcode
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: loggez
Provides-Extra: dev
Requires-Dist: raylib>=6.0; extra == "dev"
Requires-Dist: pytest>=8.4; extra == "dev"

# micronetcode + ui_cli_manager

Two packages in one repo:

- **`micronetcode`** — the low-level generic TCP command channel: `ConnectionManager`,
  `Channel`, `Codec`, `Message`, `Client`. App-agnostic; used directly by robosim.
- **`ui_cli_manager`** — the CLI-native app layer on top of `micronetcode`:
  `UICLIManager` (the `host`/`port` + `cli_commands` convenience wrapper) plus
  `CLICommand` and `ShlexASCIICodec`. For E2E tests and agents driving UI apps.

Every action doable via clicks or keyboard should be doable via the CLI. The `ui_cli_manager`
package builds the boilerplate that creates a local socket for interacting with tools such as
netcat to control the UI app.

On the app-side, the integration should be included in the IO handler exactly as `rl.IsKeyPressed`
or `IsMousePressed` would appear. Every message must be responded to.

Docs: [meehai.gitlab.io/micronetcode](https://meehai.gitlab.io/micronetcode/) — built by
[`docs/build_docs.sh`](docs/build_docs.sh) (pdoc; no sphinx/config). Build locally with
`bash docs/build_docs.sh` and open the printed `file://` link.

## Try it

```bash
python3 examples/1-raylib-hello-cli-world.py --headless   # run the app
printf 'set_text "speed: 12 m/s" 620 340\nclear_text\n' | ncat localhost 42069
```

## Usage

```python
from ui_cli_manager import UICLIManager

cli = UICLIManager.with_ascii(host="0.0.0.0", port=42069, cli_commands={"set_text": 3, "clear_text": 0})
cli.start()                       # background thread: accept + answer TCP clients

while not rl.WindowShouldClose():
    # I/O handling: polls the channel like rl.IsKeyPressed, never blocks
    cli_cmd = cli.get_cli_command()
    if cli_cmd is not None:
        if cli_cmd.command == "clear_text":
            cli_cmd.respond("Cleared all text from the UI")
        # ... apply to app state

    rl.BeginDrawing()
    # ... draw
    rl.EndDrawing()
```

Every command gets exactly one reply: `get_cli_command()` polls the channels, and `cli_cmd.respond()` sends the reply back to the waiting client (the thread blocks until you answer — never leave a command unresponded).

## Concurrency

- **Thread per client**: the listener only accepts connections and hands each one to its own daemon thread. A slow/stalled client can never starve the listener or other clients.
- **`max_connections` cap** (default 10): when every slot is taken, new connections are refused — the client receives `Server is full` and the connection is closed. `0` falls back to the default, `<0` raises `ValueError`. Pass `max_connections=N` to bound the thread count.
- **One channel per slot**: each connection owns a `Channel` (two 1-deep queues,
  `micronetcode.channel`). `get_cli_command()` polls the channels in order; each
  `cli_cmd.respond()` routes its reply back to the client that sent the command — interleaved
  clients never cross wires. Strict-channel semantics: a client can have at most one outstanding
  request; answer before sending it the next command.
- **Scripted commands** (`script_lines` / `--script`): run first, in order, before any live client command; their responses are logged, never sent to a client (they have no channel).

## Protocol

- ASCII, newline-delimited; one line = one command.
- Double-quoted arguments with spaces arrive as one argument (`shlex`).
- Lines starting with `#` are comments.
- Every command gets exactly one response; invalid input gets an error response.
- Half-close your write side (Ctrl-D / pipe EOF) to disconnect.

## Installation

Python 3.11+. The library is meant to be packaged into your main project. Just add it as a module.

Dependencies: `pip install -e .` (add `[dev]` for pytest).
For the example: `pip install raylib` as well.
