Hello World (raylib CLI)

examples/1-raylib-hello-cli-world.py is the smallest complete micronetcode program: a raylib app whose every action is also reachable over a local TCP CLI. Run it:

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

It has every moving part of a micronetcode app — the manager, its command set, the I/O handler in the main loop, and per-client color coding — and nothing else.

The manager

UICLIManager.with_ascii is the whole library in one call: host + port, the command set (command → argument count), a bound on connections, and an optional script of startup commands. .start() spawns the listener thread that accepts clients and answers them.

from ui_cli_manager import UICLIManager

cli_commands = {"set_text": 3, "clear": 0, "draw_circle": 3, "draw_line": 5,
                "save_state": 0, "get_state": 0, "exit": 0}
cli_manager = UICLIManager.with_ascii(host="0.0.0.0", port=args.port, cli_commands=cli_commands,
                                      script_lines=script_lines, max_connections=3)
cli_manager.start()

--headless just hides the window (FLAG_WINDOW_HIDDEN) so the app can run on a server with no display — the CLI is the frontend there.

I/O handling sits next to the frame poll

get_cli_command() is called where rl.IsKeyPressed / rl.IsMousePressed would be: once per frame, never blocks, and returns the next command if a client sent one. Every command is answered exactly once via respond(), and clients are colored per connection:

cli_cmd = cli_manager.get_cli_command()
if cli_cmd is not None:
    color = [rl.RAYWHITE, rl.RED, rl.BLUE][cli_cmd.message.client.channel.idx]
    if cli_cmd.command == "clear":
        cli_cmd.respond("Cleared all drawing from the UI")
        data_to_draw.clear()
    if cli_cmd.command == "set_text":
        data_to_draw.append(("text", cli_cmd.args[0], int(cli_cmd.args[1]), int(cli_cmd.args[2]), color))
        cli_cmd.respond(f"Added text: {data_to_draw[-1]} to the UI.")
    ...

Note cli_cmd.message.client.channel.idx — the message carries which client sent it, and the client carries which channel it owns, so a multi-client reply always routes back to the right socket. That is the strict-channel contract: one outstanding command per client until you respond().

Drawing is just raylib

The command handler only appends draw items; the render section is plain raylib, unchanged from a window-only app. The exit command breaks the loop (and the app).

rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
for item in data_to_draw:
    if item[0] == "text":
        text, text_x, text_y, color = item[1:]
        rl.DrawText(text.encode(), text_x, text_y, 20, color)
    elif item[0] == "circle":
        center_x, center_y, radius, color = item[1:]
        rl.DrawCircle(center_x, center_y, radius, color)
    elif item[0] == "line":
        x0, y0, x1, y1, thickness, color = item[1], item[2], item[3], item[4], item[5], item[6]
        rl.DrawLineEx((x0, y0), (x1, y1), thickness, color)
rl.EndDrawing()

Full code

Everything above, in one file. Copy it, run it, drive it from a terminal.

#!/usr/bin/env python
"""examples/1-raylib-hello-cli-world.py - minimal raylib app with a CLI command channel"""
from argparse import ArgumentParser, Namespace
import json
from loggez import loggez_logger as logger
import raylib as rl

from ui_cli_manager import UICLIManager

def main(args: Namespace):
    """main fn"""
    script_lines = []
    if args.script is not None:
        with open(args.script, "r") as fp:
            script_lines = fp.readlines()
        logger.info(f"Read {len(script_lines)} from '{args.script}'")

    cli_commands = {"set_text": 3, "clear": 0, "draw_circle": 3, "draw_line": 5,
                    "save_state": 0, "get_state": 0, "exit": 0}
    cli_manager = UICLIManager.with_ascii(host="0.0.0.0", port=args.port, cli_commands=cli_commands,
                                          script_lines=script_lines, max_connections=3)
    cli_manager.start()

    rl.SetConfigFlags(rl.FLAG_WINDOW_HIDDEN | rl.FLAG_WINDOW_UNDECORATED if args.headless else rl.FLAG_WINDOW_RESIZABLE)
    rl.InitWindow(800, 600, b"Python UI/TCP Connection Manager")

    data_to_draw: list[tuple[str, ...]] = [("text", "Hello world", 100, 100, rl.RAYWHITE)]
    while not rl.WindowShouldClose():

        # I/O handler: sits next to rl.IsKeyPressed / rl.IsMousePressed
        cli_cmd = cli_manager.get_cli_command()
        if cli_cmd is not None:
            color = [rl.RAYWHITE, rl.RED, rl.BLUE][cli_cmd.message.client.channel.idx]
            try:
                if cli_cmd.command == "clear":
                    cli_cmd.respond("Cleared all drawing from the UI")
                    data_to_draw.clear()
                if cli_cmd.command == "set_text":
                    data_to_draw.append(("text", cli_cmd.args[0], int(cli_cmd.args[1]), int(cli_cmd.args[2]), color))
                    cli_cmd.respond(f"Added text: {data_to_draw[-1]} to the UI.")
                if cli_cmd.command == "draw_circle":
                    data_to_draw.append(("circle", int(cli_cmd.args[0]), int(cli_cmd.args[1]),
                                         float(cli_cmd.args[2]), color))
                    cli_cmd.respond(f"Added circle: {data_to_draw[-1]} to the UI.")
                if cli_cmd.command == "draw_line":
                    data_to_draw.append(("line", *[int(x) for x in cli_cmd.args], color))
                    cli_cmd.respond(f"Added circle: {data_to_draw[-1]} to the UI.")
                if cli_cmd.command == "save_state":
                    with open(".state.json", "w") as fp:
                        json.dump(data_to_draw, fp, indent=2)
                    cli_cmd.respond("Saved state")
                if cli_cmd.command == "get_state":
                    cli_cmd.respond(json.dumps(data_to_draw))
                if cli_cmd.command == "exit":
                    break

            except Exception as e:
                cli_cmd.respond(str(e))

        # Drawing
        rl.BeginDrawing()
        rl.ClearBackground(rl.BLACK)

        for item in data_to_draw:
            if item[0] == "text":
                text, text_x, text_y, color = item[1:]
                rl.DrawText(text.encode(), text_x, text_y, 20, color)
            elif item[0] == "circle":
                center_x, center_y, radius, color = item[1:]
                rl.DrawCircle(center_x, center_y, radius, color)
            elif item[0] == "line":
                x0, y0, x1, y1, thickness, color = item[1], item[2], item[3], item[4], item[5], item[6]
                rl.DrawLineEx((x0, y0), (x1, y1), thickness, color)
            else:
                raise NotImplementedError(item[0])

        rl.EndDrawing()

    rl.CloseWindow()

if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--headless", action="store_true")
    parser.add_argument("--script", "-i", help="If set, run a list of commands to the UICLI manager before the socket")
    parser.add_argument("--port", type=int, default=42069)
    arg = parser.parse_args()
    main(arg)