DOCS

How to write it

Declare ports, then connect them. That is the whole idea. Every diagram below was compiled at build time by the published package from the source beside it — none was redrawn.

01Install

For the command line, install the CLI.

shell
pnpm add @love-rox/kumihimo-cli

kumihimo build studio.khm -o studio.svg   # draw it
kumihimo check studio.khm                 # validate only
kumihimo build studio.khm --watch         # redraw on save
kumihimo export studio.khm drawio         # editable draw.io file
kumihimo export studio.khm cable --stdout # cable schedule as TSV

It installs as both kumihimo and khm. Warnings do not fail the build unless you pass --strict.

There is a VS Code extension for writing it. Diagnostics as you type, and the diagram beside the source. The verdicts are the ones under "Signals and compatibility" below — it is the same compiler, running in the editor.

shell
code --install-extension love-rox.kumihimo-vscode

The preview opens with ⌘K V (Ctrl+K V on Windows and Linux) or the button in the editor title bar. It follows the editor's colour theme, and switches between the diagram and the cable, equipment and adapter schedules. Completions come from the compiler's own lists, and both the extension's words and the compiler's follow your display language. More on [the VS Code section of the front page](/en#vscode).

02Your first diagram

Declare the equipment, then wire port to port. The extension is .khm.

first.khmclean
device pc  "Laptop"  as computer { out HDMI : hdmi }
device mon "Monitor" as display  { in  HDMI : hdmi }

pc.HDMI -> mon.HDMI : hdmi 5m "V-01"
output
V-01 5mLaptopHDMIMonitorHDMI

In device <id> "<label>" as <kind>, the <id> is how connections refer to the device and is not drawn. The "<label>" is what appears on the diagram.

03Devices and ports

syntax
device <id> "<label>" as <kind> {
  in   <port spec> : <signal>   # input
  out  <port spec> : <signal>   # output
  io   <port spec> : <signal>   # bidirectional (Dante, Ethernet …)
  @model "HyperDeck Studio HD Mini"
}

A port spec takes four forms, so a sixteen-channel desk is not sixteen lines.

FormExampleExpands to
SingleSDISDI
ListL, RL, R
Numeric range1..41, 2, 3, 4
Prefixed rangeCH[1..16]CH1CH16
ports.khmclean
device mixer "Yamaha DM3" as mixer {
  in  CH[1..16] : xlr
  out L, R      : xlr
  io  DANTE     : dante
}
device amp "Amp" as amplifier { in IN_L, IN_R : trs }

mixer.(L, R) -> amp.(IN_L, IN_R) : trs 3m "A-10"
output
A-10 3mA-10 3mYamaha DM3CH1CH2CH3CH4CH5CH6CH7CH8CH9CH10CH11CH12CH13CH14CH15CH16LRDANTEAmpIN_LIN_R

Declaration order is preserved and drawn. IN 1 never ends up below IN 2. Position within a device is meaningful information, so nothing gets reordered.

Sixteen connectors drawn at one pitch read as sixteen of the same thing. Real equipment is not like that: four HDMI inputs, then four SDI, then a pair of analogue jacks, and the panel says so by leaving room between them. A gap line leaves that room above whatever is declared next.

gap.khmclean
device sw "ATEM Mini Extreme" as switcher {
  in  1..4 : hdmi
  gap
  in  5..8 : sdi
  gap 2
  in  AUDIO_L, AUDIO_R : trs
  out PGM    : sdi
  gap
  out STREAM : lan
}
output
ATEM Mini Extreme12345678AUDIO_LAUDIO_RPGMSTREAM

One gap is half a port pitch, so gap 2 is a whole one. Consecutive gaps add up. A declaration that expands into many ports gets the space once, before the first of them: gap above in CH[1..16] is one space before CH1, not sixteen down the strip.

This is presentation only. No port, connection or schedule changes — the same diagram with every gap removed describes the same system.

The kind picks the shape: camera switcher mixer recorder player display projector speaker microphone amplifier computer converter matrix patchbay router interface generic. It defaults to generic.

Any @-prefixed line is metadata. It is not drawn, but it is carried into the equipment schedule and the exports.

04Connections

syntax
<device>.<port> <arrow> <device>.<port> : <signal> <modifier>*
ArrowMeaning
->one way, in the direction signal flows
<->two way (Dante, Ethernet, control …)
--no direction (power …)

Modifiers may appear in any order and any may be omitted.

SyntaxMeaningExample
<length>cable length10m 30cm 2.5m 3ft
"<label>"cable number or name"V-01"
via "<part>"adapter or converting leadvia "HDMI-DVI cable"
[k=v, …]arbitrary attributes[connector=BNC, color=blue]
connect.khmclean
device mixer "Desk"       as mixer     { out L, R : xlr  io DANTE : dante }
device amp   "Amp"   as amplifier { in  IN_L, IN_R : trs  in AC : ac }
device netsw "Switch" as router    { io  1 : dante }
device pdu   "Power"     as generic   { out 1 : ac }

mixer.(L, R) -> amp.(IN_L, IN_R) : trs   3m  "A-10" [color=blue]
mixer.DANTE <-> netsw.1          : dante 10m "N-01"
pdu.1        -- amp.AC           : ac    2m
output
A-10 3mA-10 3mN-01 10m2mDeskLRDANTEAmpIN_LIN_RACSwitch1Power1

When both ends name the same number of ports, they pair up in order. mixer.(L, R) -> amp.(IN_L, IN_R) is the same as writing two lines.

[color=…] is the cable jacket, overriding the signal type default. It is not decoration: it is how a run gets identified on site — "the blue one into 1" — so the colour follows through into the cable schedule. Colour names work in English or Japanese, and hex such as #0af works too. Anything else becomes a diagnostic and never reaches the drawing.

Length is a bare token of a number and a unit. # is reserved for comments, so it never prefixes a length.

05Groups

A frame around a location, a rack or a subsystem.

group.khmclean
diagram "House" { direction: LR }

group stage "Stage" {
  device cam1 "FX3"  as camera     { out SDI : sdi }
  device mic1 "SM58" as microphone { out OUT : xlr }
}

group rack "Rack" {
  device sw    "ATEM" as switcher { in 1..2      : sdi  out PGM  : sdi }
  device mixer "DM3"  as mixer    { in CH[1..2]  : xlr  out L, R : xlr }
}

cam1.SDI -> sw.1      : sdi 30m "V-01"
mic1.OUT -> mixer.CH1 : xlr 20m "A-01"
output
HouseStageRackV-01 30mA-01 20mFX3SDISM58OUTATEM12PGMDM3CH1CH2LR

A group can hold another group. A venue holds a stage and a rack; the stage holds the cameras. Both levels are real to whoever walks the site, and only the innermost one names the place a box is actually standing in.

nested.khmclean
diagram "House" { direction: LR }

group venue "Hall 3" {
  group stage "Stage" {
    device cam "FX3" as camera { out SDI : sdi }
  }
  group rack "Rack" {
    device sw "ATEM" as switcher { in 1 : sdi }
  }
}

cam.SDI -> sw.1 : sdi 30m "V-01"
output
HouseHall 3StageRackV-01 30mFX3SDIATEM1

A device belongs to the group it is written in, not to that group’s parent. The equipment list names the innermost one, because that is the shelf somebody walks to.

06Signals and compatibility

This is where it stops being a drawing tool. Every connection is judged on whether it can physically work.

The verdict is reached from what the two ports themselves declare. The : <signal> on a connection describes the cable, and only fills in for an end that declares nothing. Get that backwards and a type is compared against itself, so no mismatch can ever be found.

VerdictMeaningTreated as
oka normal connectionsilent
lossyworks, but something is given up or a part is neededwarning
incompatiblewill not work without active conversionreported

The faults worth catching are the ones where the plug seats, the drawing looks right, and no signal arrives. Both cables below merely share a connector.

faults.khm2 diagnostic
device ext   "HDBaseT receiver"       as interface { out CAT : hdbaset }
device netsw "L2 switch"           as router    { in  1   : lan }
device cam   "Camera"               as camera    { out SDI : sdi }
device fs    "Frame synchroniser" as generic   { in  REF : genlock }

ext.CAT -> netsw.1 : hdbaset 20m "N-01"
cam.SDI -> fs.REF  : sdi     5m  "V-90"
output
N-01 20mV-90 5mHDBaseT receiverCATL2 switch1CameraSDIFrame synchroniserREF

Every verdict carries its reason, and the reason follows through into the cable schedule. The same trap exists for dmxxlr, rcaspdif, adatspdif, compositecomponent and wordclocksdi.

A site standard can be stated once, with its reason. The reason reaches the diagnostic and the schedule, so why a connection was allowed is never lost from the drawing.

compat
compat aes -> xlr : ok    "house standard: under 10m"
compat xlr -> rca : lossy "always through a DI"

The diagnostics you will meet most. Severities are configurable.

CodeMeaningDefault
signal-mismatchthe two ends disagree about the signalwarning
adapter-requiredan adapter is needed but not declaredwarning
adapter-insufficientvia declared but no cable can bridge thiserror
direction-mismatchoutput to output, or input to inputerror
port-overbookedmore than one source into one inputerror
implicit-devicereferred to an undeclared devicewarning
unconnected-porta declared port wired to nothingoff

Nothing throws. Every stage collects diagnostics and returns a best-effort result, so a faulty diagram still renders — a picture of a flawed system is exactly what an author needs in order to see the flaw.

07The vocabulary

Every word below is read out of the published package by this page. It is not a list copied by hand, so it cannot offer a word the compiler would reject or omit one it accepts. The VS Code completions come from the same place.

Signal types — after the : on a port or a connection.

Video

componentComponentBNC / RCA
compositeCompositeBNC / RCA
dpDisplayPortDisplayPort / DisplayPort Mini
dviDVIDVI-D / DVI-I
fiberFiberLC / SC / OpticalCON
hdbasetHDBaseTRJ45
hdmiHDMIHDMI / HDMI Mini / HDMI Micro
ndiNDIRJ45
sdiSDIBNC / DIN 1.0/2.3
st2110ST 2110RJ45 / SFP
vgaVGAD-sub 15
wireless-videoWireless videono connector

Audio

adatADATTOSLINK
aesAES/EBUXLR / BNC
danteDanteRJ45
iemIEMno connector
madiMADIBNC / SC
opticalOpticalTOSLINK
rcaRCARCA
spdifS/PDIFRCA / TOSLINK
speakonSpeakonNL4 / NL8
trrsTRRS 1/4"TRRS 1/4"
trrs35TRRS 3.5mmTRRS 3.5mm
trsTRS 1/4"TRS 1/4"
trs35TRS 3.5mmTRS 3.5mm
uhfWireless (UHF)no connector
xlrXLRXLR-M / XLR-F / Mini XLR-M / Mini XLR-F / Mini XLR-4M / Mini XLR-4F

Control

dmxDMXXLR-5 / XLR-3
gpioGPIOTerminal / D-sub
irIRno connector
midiMIDIDIN-5 / TRS 3.5mm
rs232RS-232D-sub 9
rs422RS-422D-sub 9
rs485RS-485D-sub 9 / XLR
wireless-dmxWireless DMXno connector

Network

bluetoothBluetoothno connector
lanLANRJ45
usbUSBUSB-A / USB-B / USB-C / USB Micro-B / USB Mini-B
wifiWi-Fino connector

Power

acACIEC C13 / IEC C14 / NEMA
dcDCBarrel / XLR-4
poePoERJ45
usbpdUSB PDUSB-C

Sync

genlockGenlockBNC
timecodeTimecodeBNC / XLR
wordclockWord ClockBNC

Generic

generic

A type that is not here can be declared with signal. See "Signals and compatibility".

Device kinds — after device … as.

cameraswitchermixerrecorderplayerdisplayprojectorspeakermicrophoneamplifiercomputerconvertertransmitterreceivermatrixpatchbayrouterinterfacegeneric

Jacket colours — inside [color=…]. English and Japanese spellings resolve to the same swatch, and a hex literal like #0af works too.

red
blue
green
yellow
orange
purple
black
white
graygrey
brown
pink

Length units — written against the number, with no space: 30m, 6ft.

mmcmminft

Themes — inside diagram { theme: … }.

lightdarkmonoblueprint

08Adapters

Declares that a passive adapter or converting lead sits in the run.

via.khmclean
device pc  "PC"       as computer { out HDMI : hdmi }
device mon "Monitor" as display  { in  DVI  : dvi }

pc.HDMI -> mon.DVI : hdmi 2m "V-01" via "HDMI-DVI cable"
output
V-01 2m ⇄PCHDMIMonitorDVI

via is not a way to silence a warning. It is a declaration that puts a part on the schedule. The link gets a conversion mark and the adapter appears as a line item.

For pairings a cable can genuinely bridge (HDMI↔DVI, DP→HDMI …), declaring via clears the diagnostic; leaving it out is still reported, with the required part named. For pairings no cable can bridge (SDI→HDMI …), via does not clear it. Those need a powered box, which belongs in the diagram as a device rather than as a property of a cable.

converter.khm
# Wrong: no cable turns SDI into HDMI
cam.SDI -> mon.HDMI : sdi via "SDI-HDMI converter"

# Right: the converter is a device
device conv "BMD Mini Converter SDI-HDMI" as converter {
  in  SDI  : sdi
  out HDMI : hdmi
}
cam.SDI   -> conv.SDI : sdi
conv.HDMI -> mon.HDMI : hdmi

09Wireless

A radio path is not a cable, so it carries a frequency or a channel where a cable carries a length. It is drawn dashed.

wireless.khmclean
device mic "Wireless mic" as microphone { out RF : uhf }
device rx  "Receiver"           as interface  {
  in  RF  : uhf
  out CH1 : xlr
}
device desk "Desk" as mixer { in CH1 : xlr }

mic.RF -> rx.RF     : uhf [ch=38]
rx.CH1 -> desk.CH1  : xlr 3m "A-01"
output
ch 38A-01 3mWireless micRFReceiverRFCH1DeskCH1

Wire a microphone straight into the desk with no receiver and you get told. The boundary between wireless and wired is judged too.

10A signal riding on something else

NDI is video, but what flows down the wire is Ethernet — or, over Wi-Fi, radio. The drawing is about the NDI; the physics belongs to the Ethernet or the Wi-Fi. over keeps the two apart.

over.khmclean
device cam "Roving camera"       as camera   { out WIFI : ndi }
device ap  "Access point" as router   { io  WIFI : ndi
                                             out LAN  : ndi }
device pc  "Recording PC"           as recorder { in  LAN  : ndi }

cam.WIFI -> ap.WIFI : ndi over wifi [ch=36]
ap.LAN   -> pc.LAN  : ndi over lan 20m "N-01"
output
ch 36N-01 20mRoving cameraWIFIAccess pointWIFILANRecording PCLAN

The carrier decides the physics. An over wifi hop has no connector, takes a channel, and lands on the wireless schedule. An over lan run is RJ45, takes a length and a number, and lands on the cable one. The name and the colour come from the payload.

Cable schedule

No.FromToSignalLengthSource endFar end
N-01Access pointRecording PCNDI20m

Wireless schedule

No.FromToSignalOverChannel
Roving cameraAccess pointNDIWi-Fich 36

Without over, a signal is its own carrier and everything behaves exactly as before.

11Which connector is on the box

Where a signal type offers a choice, a port can say which one it has. Gender is a property of the socket, not of the cable.

connector.khmclean
device dk "Desk" as mixer   { out CH[1..2] : xlr [connector=XLR-M] }
device sp "SP" as speaker { in  IN       : xlr [connector=XLR-F] }

dk.CH1 -> sp.IN : xlr 10m "A-01"
output
A-01 10mDeskCH1CH2SPIN

The cable ends follow. A plug mates with the opposite gender, so a male output takes a female cable end. Written once per socket, every cable reaching that socket agrees with it.

Cable schedule

No.FromToSignalLengthSource endFar end
A-01DeskSPXLR10mXLR-FXLR-M

xlr is the only builtin whose connector list is a mating pair. Everywhere else the list means "one of these" — usb is A or B or C — and there the cable end is the same name rather than an opposite.

A smaller shell is a connector, not a type. A camera has micro HDMI, a switcher has full size, and the lead between them converts nothing — so hdmi carries all three rather than splitting into three signals. Splitting them would report a mismatch on a connection that works.

shell.khmclean
device cam "SONY α7 IV" as camera   { out HDMI  : hdmi [connector="HDMI Micro"] }
device sw  "ATEM Mini"  as switcher { in  HDMI1 : hdmi [connector=HDMI] }

cam.HDMI -> sw.HDMI1 : hdmi 3m "V-01"
output
V-01 3mSONY α7 IVHDMIATEM MiniHDMI1

The reason to write it is the schedule. HDMI Micro → HDMI is a different lead from HDMI → HDMI, and it is one you either packed or did not. The same goes for DisplayPort Mini, USB Micro-B, DIN 1.0/2.3, MIDI on TRS 3.5mm, and mini XLR on a bodypack.

Cable schedule

No.FromToSignalLengthSource endFar end
V-01SONY α7 IVATEM MiniHDMI3mHDMI MicroHDMI

A part that only changes the shell is an adapter. Then the cable goes back to being an ordinary HDMI-to-HDMI, and the adapter is counted once on the parts list — which is where a thing that can be lost belongs.

shell-adapter.khmclean
device cam "SONY α7 IV" as camera   { out HDMI  : hdmi [connector="HDMI Micro"] }
adapter mh "micro-HDMI 変換アダプタ" { in IN : hdmi [connector="HDMI Micro"]  out OUT : hdmi [connector=HDMI] }
device sw  "ATEM Mini"  as switcher { in  HDMI1 : hdmi [connector=HDMI] }

cam.HDMI -> mh.IN    : hdmi
mh.OUT   -> sw.HDMI1 : hdmi 3m "V-01"
output
V-01 3mSONY α7 IVHDMImicro-HDMI 変換アダプタINOUTATEM MiniHDMI1

Cable schedule

No.FromToSignalLengthSource endFar end
V-01micro-HDMI 変換アダプタATEM MiniHDMI3mHDMIHDMI

Parts list

PartQtyBetween
micro-HDMI 変換アダプタ1SONY α7 IV / ATEM Mini

A value with a space in it goes in quotes: [connector="HDMI Micro"]. Most shell names have one.

12Moulded leads, and a length still to come

A fan-out whose tails are all moulded is a part and a cable at once. It goes in the van, it gets a number, and the person loading reads the cable schedule. as cable puts it there instead of on the parts list.

moulded.khmclean
device  dk  "Desk" as mixer { out MAIN : xlr }
adapter fan "XLR 4-way lead" as cable 5m "C-01" {
  in  IN : xlr
  out A  : xlr
  out B  : xlr
}
device sp1 "SP1" as speaker { in IN : xlr }
device sp2 "SP2" as speaker { in IN : xlr }

dk.MAIN -> fan.IN : xlr
fan.A   -> sp1.IN : xlr
fan.B   -> sp2.IN : xlr
output
DeskMAINXLR 4-way leadINABSP1INSP2IN

Cable schedule

No.FromToSignalLengthSource endFar end
C-01XLR 4-way leadDesk / SP1 / SP2XLR5m

Parts list

(empty)

One object, one row — not one per plug. It leaves the parts list, so it is not counted twice. as cable takes a length and a cable number, the same ones a run takes.

Where the length is not settled yet, write ?m. Leaving it off already worked, and the blank it produced meant "not measured" and "nobody thought about it" at once. On a list somebody packs a van from, only one of those is a job still to do.

unknown.khmclean
device dk  "Desk"   as mixer   { out L, R, SUB : xlr }
device sp1 "SP1"  as speaker { in IN : xlr }
device sp2 "SP2"  as speaker { in IN : xlr }
device sub "Sub" as speaker { in IN : xlr }

dk.L   -> sp1.IN : xlr 15m "A-01"   # 測った
dk.R   -> sp2.IN : xlr ?m  "A-02"   # ケーブルは要る。長さはこれから
dk.SUB -> sub.IN : xlr     "A-03"   # 長さについて何も言っていない
output
A-01 15mA-02 ?mA-03DeskLRSUBSP1INSP2INSubIN

Cable schedule

No.FromToSignalLengthSource endFar end
A-01DeskSP1XLR15m
A-02DeskSP2XLR?m
A-03DeskSubXLR

The unit is still written, because which unit it will be measured in is not the open question. ?m, ?ft, and any other unit the language knows.

13Equipment libraries

A desk's sixteen channels do not get rewritten per drawing. Define it once with model, instantiate as many as you like with device … from.

library.khmclean
model dm3 "Yamaha DM3" as mixer {
  in  CH[1..16] : xlr
  out L, R      : xlr
  @vendor "Yamaha"
}

device foh from dm3
device mon from dm3 "Monitor desk"
output
Yamaha DM3CH1CH2CH3CH4CH5CH6CH7CH8CH9CH10CH11CH12CH13CH14CH15CH16LRMonitor deskCH1CH2CH3CH4CH5CH6CH7CH8CH9CH10CH11CH12CH13CH14CH15CH16LR

Definitions in another file are pulled in with use. If an imported file also held devices or connections they are ignored, with a warning: a library is where definitions live, not drawings.

studio.khm
use "lib/yamaha.khm"

device foh from dm3 "FOH desk"

14Appearance

diagram block
diagram "Studio A" {
  direction: LR      # LR (left to right, default) | TB (top to bottom)
  theme: light       # light (default) | dark | mono | blueprint
  spacing: 60        # gap between nodes, px
}
ThemeFor
lightDefault. Screen and colour print
darkDark screens
monoBlack and white print and photocopies
blueprintBlueprint colouring, as facility drawings use

mono uses no colour at all. Signals are distinguished by line style instead, and a jacket colour given with [color=…] is ignored — pretending a colour survived a photocopy helps nobody.

mono.khmclean
diagram "For monochrome printing" { theme: mono }

device cam "Camera"       as camera   { out SDI : sdi }
device sw  "Switcher" as switcher { in 1 : sdi  out PGM : sdi }
device rec "Recorder"   as recorder { in SDI : sdi }

cam.SDI -> sw.1    : sdi 30m "V-01" [color=blue]
sw.PGM  -> rec.SDI : sdi 2m  "V-10"
output
For monochrome printingV-01 30mV-10 2mCameraSDISwitcher1PGMRecorderSDI

A theme can also be passed with -t/--theme, but a diagram { theme: … } in the source wins: the drawing knows how it is meant to look, the caller only knows a default.

15Output and embedding

Besides SVG, it exports an editable draw.io file and TSV cable and equipment schedules.

Markdown takes a kumihimo code fence directly; it becomes SVG at build time.

markdown
```kumihimo
cam.SDI -> sw.1 : sdi 30m "V-01"
```

import rehypeKumihimo from '@love-rox/kumihimo-rehype';

unified()
  .use(remarkParse)
  .use(remarkRehype)
  .use(rehypeKumihimo, { theme: "dark", onDiagnostics: report })
  .use(rehypeStringify);

There are React, Vue and Astro components too. Compiling is asynchronous, so the previous diagram stays on screen while a new one is produced, and a slow earlier compile can never overwrite a newer one.

React
import { Kumihimo, useKumihimo } from '@love-rox/kumihimo-react';

<Kumihimo source={src} theme="dark" onDiagnostics={report} />;

const { svg, diagram, diagnostics, pending, error } = useKumihimo(src);

Use onDiagnostics if you would rather not publish a nice-looking picture of faulty wiring.