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 — built by docs/build_docs.sh (pdoc; no sphinx/config). Build locally with bash docs/build_docs.sh and open the printed file:// link.

Try it

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

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.