printcontrol
A keyboard-driven TUI for monitoring and controlling a 3D printer over USB
serial. One static Go binary, zero runtime dependencies, theme-aware via
your terminal's 16-colour palette — designed to feel at home on Omarchy
alongside wiremix, lazygit, lazydocker, and friends.
The model: the printer is in charge during an SD print, we observe and adjust. Start a print from the LCD, the SD card, or printcontrol's own SD picker — then live-monitor temps, progress, and ETA, and tweak feedrate, flow, fan, hotend / bed setpoints, or babystep Z without ever leaving the keyboard. Quit and come back later; the printer keeps printing and printcontrol rediscovers the in-progress job on reconnect.
┌─ printcontrol · 3D printer TUI ────────────────────────────────────────┐
│ ● Status │ ● Temperatures │
│ Connection: /dev/ttyUSB1 @ 115200 │ Hotend: 201.3 / 210.0 °C │
│ · Marlin 2.1.2 │ Bed: 60.2 / 60.0 °C │
│ File: benchy.gcode SD PRINT │ │
│ Progress: [██████████··········] │ set hotend: [h] │
│ ETA: 1h 12m │ set bed: [b] │
├─────────────────────────────────────┴────────────────────────────────────┤
│ ● Controls │
│ Feedrate: 110% [-/+] [0] Flow: 100% [ [ / ] ] Fan: 30% [f/F] │
│ Babystep Z: -0.050 [{ / }] [Z] │
│ press [g] to send raw G-code │
├──────────────────────────────────────────────────────────────────────────┤
│ ● Log │
│ > M105 │
│ < ok T:201.3 /210.0 B:60.2 /60.0 @:127 B@:0 │
├──────────────────────────────────────────────────────────────────────────┤
│ [c] connect [d] disconnect [s] sd start [p] pause [r] resume [x] cancel │
│ [h] hotend [b] bed [g] gcode [q] quit │
└──────────────────────────────────────────────────────────────────────────┘
Install
Build from source — needs Go 1.22+. The repo's canonical install target is
~/.local/bin (matches Omarchy's PATH); adjust GOBIN if you want it
elsewhere.
cd go
GOBIN=$HOME/.local/bin go install ./cmd/printcontrol
You need permission to open the serial port. On Arch / Omarchy that means
membership in the uucp group:
groups | grep -q uucp || sudo usermod -aG uucp "$USER" # then log out and back in
Verify the printer is visible:
ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null
Run
printcontrol # auto-detect port AND baud
printcontrol -port /dev/ttyUSB1 # lock the port, still auto-baud
printcontrol -port /dev/ttyUSB1 -baud 250000
printcontrol -no-connect # launch without touching serial
Flags
| Flag | Default | Description |
|---|---|---|
-port |
(auto) | Serial device path. Scans /dev/ttyACM* + /dev/ttyUSB* if empty. |
-baud |
0 |
Baud rate. 0 = auto-detect from [250000, 115200, 230400, 500000, 57600]. |
-no-connect |
false |
Skip auto-connect on launch. |
Auto-detect
With no flags, printcontrol scans every plausible serial device and probes
each candidate baud until something replies with an unmistakably
printer-shaped line (Marlin, FIRMWARE_NAME, RepRapFirmware, Klipper,
or a T:.../... temperature report). Two-letter tokens like "ok" are
not accepted as a match because they appear too easily in
misaligned-baud garbage. Worst-case probe time is ~15 s; first match wins
and the rest are skipped.
The connect flash at the bottom of the screen reports the chosen
combination — e.g. connected: /dev/ttyUSB1 @ 115200.
Keys (top-level)
| Key | Action |
|---|---|
c |
Connect / reconnect |
d |
Disconnect |
s |
List SD card and pick a file to print |
p |
Pause SD print (M25) |
r |
Resume (M24) |
x |
Cancel SD print (M524) |
h |
Focus hotend setpoint input |
b |
Focus bed setpoint input |
g |
Focus raw G-code input |
+ / - |
Feedrate ±10% |
0 |
Feedrate back to 100% |
[ / ] |
Flow ±5% (M221) |
f / F |
Fan ±10% |
{ / } |
Babystep Z ∓0.05 mm (M290, Marlin) |
Z |
Reset babystep to zero |
q |
Quit |
When a text input is focused: type the value, Enter to commit, Esc to
cancel.
SD start workflow (key: s)
- Press
s. printcontrol sendsM21(mount SD) andM20(list files). - A picker appears in place of the Log panel.
↑/↓(ork/j) to highlight,Enterto start the print,Escto dismiss without printing.- Selecting a file sends
M23 <filename>thenM24. The print runs from the SD card, autonomously of printcontrol. - You can
qimmediately after — the printer doesn't care. Reconnect any time; theM27poll discovers the in-progress print within ~5 seconds and Status / Progress / ETA repopulate.
Notes:
- The picker shows the long filename if the firmware reports one, but
always passes the short 8.3 name to
M23(that's what Marlin requires). - Nested folders are listed but can't currently be entered. File a request if you need it.
Register with the Omarchy launcher
Once you're happy with it, drop a desktop entry so SUPER+SPACE finds it:
omarchy-tui-install printcontrol printcontrol float <icon-url>
float makes Hyprland treat it as a floating window — usually the right
call for a single-pane TUI. Switch to tile if you'd rather have it in
your tiling layout.
What works · what doesn't
Works today
- Auto-detect serial port and baud rate
- Connect / disconnect over USB serial (multi-port scan)
- Live temperatures (hotend, bed, chamber if present) every 2 s
- SD-card print detection, progress %, ETA
- SD card file listing (
M20) and start (M23+M24) from the TUI - Pause / resume / cancel (
M25/M24/M524) - Temperature setpoints (
M104/M140) - Fan, feedrate, flow controls (
M106/M220/M221) - Babystep Z (
M290— Marlin) - Raw G-code entry
- Firmware identification (
M115) - Adaptive log panel (footer stays visible on short terminals)
Not yet
- Host-side print streaming (sending a
.gcodefile from the computer). Needs the line-numbered protocol with checksum + resend handling. - Klipper-specific babystep (
SET_GCODE_OFFSET). SendsM290today, which Klipper rejects. - SD folder navigation (
M20 <dir>). - Octoprint / Moonraker backends. Architecture supports it; see
ARCHITECTURE.md. - Persisted profiles (per-printer last setpoints, preferred port).
Troubleshooting
"no responsive printer on [...]" — Auto-detect probed every
ttyACM/ttyUSB device at every common baud and didn't find a printer
signature. Most often this means another program (OctoPrint, Klipper's
klippy, pronsole) already owns the port. Quit it first, or pass
-port explicitly. If you have an unusual baud (e.g. 1000000), pass
-baud to override the probe.
"could not auto-detect baud rate" — printcontrol opened the port but
saw no recognisable reply at any candidate baud. Confirm the device is
actually a printer (screen /dev/ttyUSB1 115200, then send M115); also
common: the printer is mid-firmware-flash, or the USB cable is power-only.
Temps show but progress stays at 0% — M27 only reports SD progress.
If you started the print from a host (OctoPrint, etc.) progress won't
appear here.
Babystep does nothing — You're likely on Klipper, which doesn't
implement M290. Klipper support is on the roadmap.
Mangled lines like TT::57.4957.49 — This was the symptom of Marlin's
auto-temperature report colliding with our explicit M105 poll. Fixed
upstream: the prime block now sends M155 S0 / M27 S0 to disable
auto-reports.
Layout
printcontrol/
├── go/
│ ├── cmd/printcontrol/main.go # entry point, flag parsing
│ ├── internal/printer/ # serial driver + G-code state machine
│ │ ├── state.go # State / PrintState / TempPair / SDFile
│ │ ├── parse.go # line parsing (M105, M27, M115, M20, ...)
│ │ ├── printer.go # Connect/Disconnect, owning goroutine, probe
│ │ └── parse_test.go
│ └── internal/tui/ # Bubble Tea model + Lipgloss styling
│ ├── model.go # Model, key handling, SD picker, layout
│ └── style.go
├── README.md
└── ARCHITECTURE.md # protocol + design notes
See ARCHITECTURE.md for the wire protocol, the state model, the
auto-detect probe strategy, and why things are wired the way they are.
License
GPL-3.0-or-later (matches Printrun, which inspired the protocol layer).