Vibe-coded project: I built this tool through an iterative conversation with an AI coding assistant. I described what I wanted, ran the results against my printer, and asked for changes. The assistant wrote and revised the code. This is a personal experiment, not an official Elegoo application or a production-ready tool.
The slicer was open. My Elegoo Centauri Carbon was printing. I also had a web page open with the printer’s status.
Everything I needed was already available, but I kept switching away from my terminal to check it. How far along was the print? How much time was left? Did everything still look okay?
I spend a lot of my time in the terminal. Having the printer’s status there felt natural. I wanted to glance at it, then carry on with what I was doing. And when the print finished, I wanted a notification so I could go collect it.
It was a small annoyance, and exactly the kind of personal tool I might normally put off building. Useful, yes, but did I really want to spend time working on it?
This time, I opened a conversation with Codex, using GPT-6 Astra.
The first request was simply to get the printer’s status in the CLI. Once that worked, I wanted a proper TUI. Then a progress bar. Then colors. Then I remembered the printer had a camera.
The scope grew one “can we also…” at a time.
The AI chose pycentauri, which provides a CLI and a Python API for communicating with the printer. My printer’s local address is 192.168.2.89; replace that with your own IP if you try the commands.
First, we checked that we could retrieve its status:
centauri status --host 192.168.2.89
Then we watched for updates:
centauri watch --host 192.168.2.89
It worked. The printer was talking to the terminal.
That gave us a useful starting point: the connection and data were available, so the custom code could focus on presentation and notifications.
On the Python side, the basic connection looks like this:
async with await Printer.connect(host) as printer:
st = await printer.status()
The returned status object provides values such as st.progress, st.filename, st.print_status, and temperatures. For continuous updates, the library exposes printer.watch(), an asynchronous iterator.
Once I could see the status, I wanted something easier to read at a glance.
We built the interface with Rich. The script’s dashboard() function takes a status object, the printer address, a rendered camera image, and a language code. It returns a layout for the whole screen.
Rich’s building blocks map neatly to the job:
Layout divides the screen into rows and columns.
Panel gives each section a border and title.
Table aligns labels and values.
Text supplies colors and styles.
Live keeps the interface displayed as data changes.
For example, the print details use a small grid:
info = Table.grid(expand=True, padding=(0, 1))
info.add_column(style="dim", width=12)
info.add_column(style="bold bright_white")
info.add_row(words["file"], st.filename or "—")
The full dashboard puts the current state at the top, followed by progress. Below that are print details and temperatures on the left, with the camera on the right. Fan speeds sit in the footer.
The rendering function builds the screen; the main loop feeds it fresh data:
live.update(dashboard(st, host, frame, lang), refresh=True)
This made each visual adjustment straightforward. We could change the layout without changing how the printer connection worked.
My next request was a proper progress bar and some color.
The implementation is small enough to understand immediately:
value = max(0, min(100, value))
filled = round(width * value / 100)
colour = "green" if value >= 90 else "bright_cyan"
bar = Text(justify="center")
bar.append("█" * filled, style=f"bold {colour}")
bar.append("░" * (width - filled), style="grey30")
The percentage is kept between zero and one hundred, then converted into filled cells across a default width of 56 characters. The bar stays cyan until 90%, when it turns green. The percentage appears above it.
Temperatures and printer states also get their own colors. A few emojis make states such as sleeping, printing, and completed easier to recognize.
These were small changes, but they made the dashboard much closer to what I had wanted: something I could check without interrupting my train of thought.
The notification needed a little memory. Checking whether the current state says “completed” on every update would risk announcing the same job repeatedly.
The script tracks whether it has observed an active print and remembers the previous status. Here is the completion branch, with the error-notification branch omitted for clarity:
if code in ACTIVE:
seen_active = True
if seen_active and code != previous:
if code == 9:
notify(
I18N[lang]["done_notification"].format(
filename=st.filename or I18N[lang]["unknown_file"]
)
)
seen_active = False
previous = code
When an observed print reaches status 9, the completed state used here, the script sends a notification and clears the active flag. Opening the TUI after a job has already finished does not immediately announce a completion it never observed.
On my Mac, notify() calls osascript to display a notification titled “Centauri Carbon,” with a sound. The application also handles the error state with a separate notification.
This part is macOS-specific, and the TUI must remain running to observe the transition. For my setup, that was enough.
This was where the conversation became more experimental.
The printer already had a camera. Could we put its image in the TUI too? I suggested libcaca.
The first attempts produced an image, but I could barely recognize anything in it. I asked for improvements, tried again, and still found it hard to read. I asked whether libcaca would solve it.
The AI eventually chose Chafa as the main renderer.
The script requests a JPEG snapshot from the printer, then passes the image bytes to Chafa through standard input. The subprocess arguments are:
[
"chafa",
"--format=symbols",
"--colors=full",
"--symbols=block+braille+half+quad",
f"--size={width}x{height}",
"--scale=max",
"--animate=off",
"--probe=off",
"-",
]
Chafa returns colored terminal symbols. Rich’s Text.from_ansi() converts the output into an object we can put inside the camera panel.
Using blocks, Braille patterns, half blocks, and quadrants gave us a more recognizable image in my terminal. The script also calculates the image size from the terminal width at startup, within minimum and maximum bounds.
There is a Pillow-based fallback if Chafa is unavailable or fails. It adjusts the image and maps pixel brightness to characters, keeping each pixel’s RGB color. But Chafa was the improvement that made the camera view useful for me.
The feedback loop mattered here. Code that successfully draws an image can still produce an image you cannot read. I had to look at it in my actual terminal to judge the result.
Two final adjustments made the tool fit my workflow better.
First, language support. The interface started in French, and I asked for English too. A small I18N dictionary holds translated labels and notification messages:
words = I18N[lang]
info.add_row(words["file"], st.filename or "—")
Automatic selection checks LC_ALL, then LANG, choosing French for a value starting with fr and English otherwise. I can also select the language explicitly with --lang en or --lang fr.
Second, I asked to slow the camera down to one refresh per minute.
After getting the image readable, I realized I did not need it changing constantly beside my work. A print evolves slowly; an occasional snapshot suits the job.
The script uses time.monotonic() to check whether another snapshot is due. The default interval is 60 seconds, and the previous image stays visible between requests. The check happens when status updates arrive, so the interval is approximate. Snapshot retrieval also happens inside the watch loop and can briefly delay processing status updates.
The result is a periodically refreshed camera view alongside live printer data.
Then, while using it, I noticed the TUI had stopped twice.
Inspecting the code revealed a plausible failure path: the library’s status stream can end when the connection closes, and our original loop would simply finish with it. We did not have evidence proving that caused both incidents, but the missing reconnection handling was real.
We added a watch_reconnecting() function around the connection and status stream. It waits up to 45 seconds for the next update:
st = await asyncio.wait_for(
anext(updates),
timeout=STATUS_TIMEOUT,
)
If the connection fails, the stream ends, or updates stall, it logs the failure, displays a reconnection message, and retries. Consecutive failed attempts wait 2, 4, 8 seconds, and so on, up to 30 seconds. Receiving a status update resets that delay.
Notification tracking remains outside the reconnect loop, so it survives a temporary disconnection. A rotating log beside the script gives us something to inspect if another issue appears.
Simulated connection failures, ended streams, timeouts, and recovery checks passed. That gives us some confidence in those paths, although it is no substitute for leaving the tool running through real prints.
This was a follow-up fix after the initial build—and a useful reminder of what a quick first version can miss.
The application is a single Python script with a small launcher named imprimante, French for “printer.” In my installation:
~/.local/bin/imprimante
~/.local/share/centauri-tui/centauri_tui.py
The launcher uses the Python interpreter from the pycentauri pipx environment. With the script, launcher, and dependencies installed, I start it with:
imprimante --host 192.168.2.89 --lang en
For a different camera interval:
imprimante --host 192.168.2.89 --lang en --camera-interval 30
For a single status display and snapshot attempt, followed by exit:
imprimante --host 192.168.2.89 --lang en --once
The tool is read-only: it monitors the printer without exposing controls to start, pause, or cancel a job.
It is far from perfect. The notification backend only supports my Mac. The camera is a terminal approximation, and failed snapshots can leave an old image on screen without an obvious warning. There is still plenty to improve if I decide to make it something other people can easily install and use.
But it does the job. I can stay in my terminal, glance at the printer’s progress, check the camera, and get a notification when the print finishes.
For a tool vibe-coded in about 15 minutes, followed by a bit of real-world debugging, I think that is pretty cool.
As a developer, I am genuinely impressed. Building this myself would have taken me a few hours: finding the right libraries, connecting them, putting together the layout, experimenting with image rendering, and getting the details into a usable state.
I still had to decide what I wanted, test the result, and notice when something was wrong. But the time between “this would be handy” and having it running beside my work was remarkably short.
The printer was already printing. Now I can get back to what I was doing and let the terminal tell me when it is done.