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 ofmicronetcode:UICLIManager(thehost/port+cli_commandsconvenience wrapper) plusCLICommandandShlexASCIICodec. 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_connectionscap (default 10): when every slot is taken, new connections are refused — the client receivesServer is fulland the connection is closed.0falls back to the default,<0raisesValueError. Passmax_connections=Nto 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; eachcli_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.