seqdraw

    Render sequence diagrams as terminal text: a small DSL, a layout engine, and a CLI.

    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    2

    #SeqDraw

    Render sequence diagrams as terminal text. Write a five-line description of a call flow, get an aligned diagram you can paste into a README, a CI log, a pull request description or a terminal.

    ┌────────┐ ┌────────┐ │ Client │ │ Server │ └────┬───┘ └────┬───┘ │ ping │ │────────────>│ │ pong │ │┄┄┄┄┄┄┄┄┄┄┄┄>│ │ │

    #Why this exists

    Diagrams that live in a browser do not survive a terminal, a CI log or a code review. SeqDraw keeps the source in the repository as plain text and produces output that is readable wherever plain text is readable, with no runtime, no image files and no font fallback to worry about.

    The value is in the layout: columns are sized from the labels, lifelines keep their position across every row, and double-width characters (Chinese, Japanese, Korean, emoji) advance exactly two cells so the box borders stay aligned.

    #Install

    moon add LMK-ai-nb/seqdraw

    #Command line

    moon run cmd/main -- examples/login.seq

    The command reads one file and prints the diagram. The grammar is documented below; examples/ holds three runnable diagrams.

    #Grammar

    One construct per line. Blank lines, # and // comments are ignored.

    LineMeaning
    participant A as Clientdeclare a column; the label is optional and defaults to the id
    A -> B: textsolid arrow with a filled head
    A --> B: textdashed arrow
    A ->> B: textsolid arrow with an open head
    A -->> B: textdashed arrow with an open head
    A -> A: textself message, drawn as a loop
    note over A: textnote box over one column
    note over A,B: textnote box spanning two columns
    alt label / else label / endconditional block
    loop label / endrepeated block
    opt label / endoptional block

    Participants are created on first use, so the participant line is only needed when the label differs from the id. Blocks nest.

    #Library

    match @seqdraw.parse(source) {
    Err(err) => println(err.to_text())
    Ok(diagram) => println(@seqdraw.render(diagram))
    }

    parse returns a Diagram of participants and items; render turns it back into text. Both are pure: no file or terminal access, so the renderer runs on wasm, wasm-gc, js and native.

    #Examples

    FileShows
    examples/login.seqthree columns, a conditional block, a note
    examples/payment.seqself messages, a nested loop inside a condition
    examples/pipeline.seqshort labels and a two-branch hand-off

    #Design notes

    • Column geometry. The space between two lifelines is the smallest distance that still fits the widest label crossing it; labels spanning several columns push the involved gaps until the text fits. This is computed once, before any row is painted.
    • Cell grid. Rows are arrays of cells, not strings, so a wide character can mark the cell it covers as occupied without faking a space. The final string drops those markers, which is why box borders line up under CJK text.
    • Group frames. Nesting depth decides how far a frame is inset; the frame is painted after its body, so the vertical borders pass over every row it owns.

    #Boundaries

    • Text only. No colours, no box styles, no themes.
    • One note per line; multi-line notes are not implemented.
    • Long labels widen the canvas instead of wrapping.
    • The DSL is read line by line; a construct cannot span two lines.

    #Tests

    moon check --target wasm-gc --deny-warn moon test --target wasm-gc

    The suite covers width accounting for wide characters, clipping, parsing, line-numbered errors, unclosed blocks and a golden rendering.

    #License

    Apache-2.0. See LICENSE.

    ArrowStyle

    pub(all) enum ArrowStyle {
    Solid
    Dashed
    }

    How a message line is drawn.

    Diagram

    pub(all) struct Diagram {
    participants : Array[Participant]
    items : Array[Item]
    }

    GroupStart

    pub(all) struct GroupStart {
    kind : String
    label : String
    }

    Item

    pub(all) enum Item {
    Message(Message)
    SelfMessage(SelfMessage)
    Note(Note)
    GroupStart(GroupStart)
    GroupElse(String)
    GroupEnd
    }

    One item in the diagram body.

    Message

    pub(all) struct Message {
    from : String
    to : String
    label : String
    style : ArrowStyle
    open_head : Bool
    }

    Note

    pub(all) struct Note {
    from : String
    to : String
    text : String
    }

    ParseError

    pub(all) struct ParseError {
    line : Int
    message : String
    }

    A parse error carries the 1-based line number so the CLI can point at it.

    ParseError::to_text

    fn ParseError::to_text(self : ParseError) -> String

    Render the error for the command line.

    Participant

    pub(all) struct Participant {
    id : String
    label : String
    }

    One participant column.

    SelfMessage

    pub(all) struct SelfMessage {
    who : String
    label : String
    style : ArrowStyle
    }

    char_width

    fn char_width(c : Char) -> Int

    Display width of a character in a terminal cell.

    The table below covers the ranges a diagram label realistically uses. It is deliberately a range table rather than a Unicode database: the renderer only needs to keep columns aligned, and every range here is documented so the boundary is explicit instead of guessed.

    clip_to_width

    fn clip_to_width(text : String, limit : Int) -> (String, Int)

    Slice a string so that its display width does not exceed limit. Returns the prefix and the width actually used.

    pad_center

    fn pad_center(text : String, target : Int) -> String

    Pad on both sides as evenly as possible.

    pad_right

    fn pad_right(text : String, target : Int) -> String

    Pad with spaces on the right until the display width reaches target.

    parse

    fn parse(source : String) -> Result[Diagram, ParseError]

    Parse the line-oriented DSL into a diagram.

    render

    fn render(d : Diagram) -> String

    Render a parsed diagram as text.

    text_width

    fn text_width(text : String) -> Int

    Display width of a whole string.

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io