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)