No description
  • Go 95.7%
  • Just 2.3%
  • Shell 2%
Find a file
Viktor Varland aa5610fb57
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
docs: changelog for v1.1.0
2026-06-26 09:21:52 +02:00
.just chore: update wrangle regen command to wrangle init 2026-06-26 09:19:54 +02:00
.woodpecker ci: add Woodpecker build + release pipelines and changelog tooling 2026-06-24 13:50:57 +02:00
scripts chore: update wrangle regen command to wrangle init 2026-06-26 09:19:54 +02:00
.gitignore Fix --vertical reading direction (was 180° off) 2026-06-19 14:08:29 +02:00
args.go refactor: migrate to the gout/cli command framework 2026-06-24 12:47:48 +02:00
CHANGELOG.md docs: changelog for v1.1.0 2026-06-26 09:21:52 +02:00
device.go Pace long labels with status handshake (fixes dropped tails) 2026-06-19 13:40:36 +02:00
go.mod feat: add shell completion command 2026-06-25 11:43:19 +02:00
go.sum feat: add shell completion command 2026-06-25 11:43:19 +02:00
justfile chore: replace release pipeline with wrangle 2026-06-25 09:31:05 +02:00
main.go feat(completion): complete print's flags and values 2026-06-25 12:57:38 +02:00
protocol.go Fix margins/centering, add --vertical 2026-06-19 13:21:43 +02:00
README.md Pace long labels with status handshake (fixes dropped tails) 2026-06-19 13:40:36 +02:00
render.go Fix --vertical reading direction (was 180° off) 2026-06-19 14:08:29 +02:00
verify_test.go Pace long labels with status handshake (fixes dropped tails) 2026-06-19 13:40:36 +02:00

dymo

A small Go CLI for the DYMO LabelManager 280 (USB 0922:1005).

just build         # or: go build -o dymo .
just install       # installs to /usr/local/bin
dymo print "print this text"

It renders text to a bitmap and speaks DYMO's "matrix" raster protocol straight to the kernel usblp character device (/dev/usb/lp0) — no libusb, no driver detaching. The protocol/bit-packing is verified byte-for-byte against the labelle reference implementation.

Usage

usage: dymo <command> [flags]

commands:
  print     render text and print it to a DYMO LabelManager 280
  info      list detected DYMO / usblp devices
  version   print version

Print flags

Flag Default Meaning
-tape 12 Tape width in mm: 6, 9, 12, 19
-size fill tape Text size: xs/sm/m/l/xl/xxl, xsmall..xxlarge, or a point size like 8/10/12/16/20/24
-vertical off Rotate text 90° for labels read along a vertical surface (e.g. a cable)
-flip off Rotate the label 180° (read the other way up)
-font built-in Go Bold Path to a TrueType/OpenType font
-margin 0 Extra margin (mm) on each side, beyond the cutter gap
-gap 9.5 Print head-to-cutter distance (mm); sets the minimum, symmetric margin
-dottab -1 Vertical bias in bytes; -1 auto-centres narrow tape on the head
-device autodetect usblp device path
-cut off Append auto-cut command (LM280 normally cuts manually)
-o Dump raw protocol bytes to a file instead of printing
-preview Write a PNG preview of the rendered label
-dry-run off Render only; send nothing
-v off Verbose

Examples

./dymo print "Hello"
./dymo print -tape 9 "9mm tape"
./dymo print -size xl "big text"
./dymo print -size 12 "12 point"
./dymo print -flip "upside down"
./dymo print -tape 9 -vertical "cable-1"     # rotated 90° for a cable
./dymo print 'WiFi\npassword: hunter2'      # two lines
./dymo print -font /usr/share/fonts/TTF/DejaVuSans.ttf "custom font"
./dymo print -dry-run -preview out.png "check before printing"
echo "from stdin" | ./dymo print

Margins and the cutter gap

The print head sits a fixed distance behind the cutter (the gap), so the leading margin can never be smaller than that gap — the printer can't reverse-feed. To get equal margins, the tool feeds 2 × gap of trailing tape so the trailing margin matches the leading one:

leading blanks  = margin            visible leading  = gap + margin
trailing blanks = margin + 2·gap     visible trailing = gap + margin

The default -gap 9.5 is calibrated from measured prints on a LabelManager 280. If your trailing margin comes out larger than the leading one, lower -gap; if it's smaller (text cut too soon), raise it. -margin adds equal space to both sides on top of that.

Vertical centering

On tape narrower than the print head (anything under 12 mm), the print band is biased to the centre of the head with the ESC B dot-tab command so the text sits centred across the tape. This is automatic (-dottab -1); pass an explicit -dottab 0 to disable it, or another value to nudge the band up/down in 1-byte (8-dot) steps.

Permissions

usblp exposes /dev/usb/lp0 as root:lp (mode 660). Either run with sudo, join the lp group, or install a udev rule:

echo 'SUBSYSTEM=="usbmisc", ATTRS{idVendor}=="0922", MODE="0660", GROUP="lp"' \
  | sudo tee /etc/udev/rules.d/91-dymo.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo usermod -aG lp "$USER"     # then re-login

Re-plug the printer afterwards.

How it works

180 DPI; the printable height is int(8 * tape_mm / 12) bytes → 8 bytes (64 dots) for 12 mm tape. The label is a stream of vertical columns:

ESC C 0          tape colour / init
ESC D <n>        n bytes per column
SYN <n bytes>    one printed column (repeated, left → right)
...
ESC A            status request / end of label

Each column packs pixels MSB-first with y = H-1-i, matching labelle's image.transpose(ROTATE_270).tobytes(). Blank columns are added before and after the text to produce symmetric margins around the cutter gap (see Margins and the cutter gap).

Notes / limitations

  • The LM280 has a manual cutter; -cut is provided for completeness but may be a no-op on this model.
  • Long labels are paced with a status handshake: before each batch of ~64 columns the tool sends ESC A and waits for the printer to reply (it only does so once it has buffer room), so the tail of long labels isn't dropped. -no-handshake streams without it (faster, but long labels may lose their end).