SourceThe websiteFrom init to eject

Part X, Chapter 9

Part X, Chapter 9: From init to eject

Package: site/src/

Language: TypeScript

Raw source

From init to eject

Four commands are the whole of Loom's surface, and this section shows what each one does to a directory rather than describing it. A reader sees a corpus scaffolded, written out to source, rendered as a document, and finally left behind — and the pictures are file trees, because a file tree is what actually changes.

Each command carries its own colour, the same one its pill wears everywhere else on the site, and each block can be copied straight off the page.

The four commands

A command is a pill, an illustration, and a sentence. The pill is the command itself — pressing it copies the text, and a label beside it fades in to say so, then fades back.

The pill's ground is its own colour at a tenth or a thirteenth strength, dark enough to read as a chip and light enough that four of them in a row do not fight.

TypeScript
import { Array, Match } from 'effect'
import type { Html, HtmlBuilder } from 'foldkit/html'
import { Copied, type Message, type Model } from './model'
import { broadSeam, carpetMark, operationsCarpet } from './components'
import initDrawing from './init.svg?raw'
import tangleDrawing from './tangle.svg?raw'
import ejectDrawing from './eject.svg?raw'

type Operation = {
  readonly command: string
  readonly colour: string
  readonly wash: string
  readonly says: string
  readonly drawing?: string
  readonly json?: ReadonlyArray<ReadonlyArray<readonly [string, string]>>
}

What each command does to a directory

loom init scaffolds. It writes the workspace's own directory and the configuration inside it, and leaves you to make the corpus it names.

loom tangle writes the code out. Four documents become four files, and the files are not all one language — a section that holds SQL writes SQL, and one that holds JSON writes JSON.

loom weave renders the same corpus the other way, as one document: its parts, how many sections it holds, and a pointer into every file it tangles.

loom eject leaves. The corpus is archived beside the project and the source stays exactly where it was, still running.

Three of the four are drawn as vendored SVGs, applied exactly as the design made them. Init stands still, since a directory that has just been scaffolded has nothing to show happening. Tangle and eject each run a loop of six and a fifth seconds, in SMIL, driven by nothing on this page — no clock in the model, no timer, no waiting for a scroll. Weave alone is not a drawing but the document it produces, so it is built here.

TypeScript
export const operations: ReadonlyArray<Operation> = [
  {
    command: 'loom init',
    colour: '#00996B',
    wash: 'rgba(0, 153, 107, 0.13)',
    says: 'Scaffolds the workspace: a .loom directory holding its config.yaml, and a corpus you make for the documents you write — read in order, top to bottom.',
    drawing: initDrawing,
  },
  {
    command: 'loom tangle',
    colour: '#2E6FF2',
    wash: 'rgba(46, 111, 242, 0.10)',
    says: 'A section names the file its code belongs to. One document can fill several files, and several documents can fill one.',
    drawing: tangleDrawing,
  },
  {
    command: 'loom weave',
    colour: '#8B5CF6',
    wash: 'rgba(139, 92, 246, 0.10)',
    says: 'The whole corpus as one document — prose, headings, bindings, and a pointer into each tangled file. Any site generator can render it.',
    json: [
      [['{', 'punctuation']],
      [
        ['  "parts"', 'key'],
        [': [', 'punctuation'],
        ['"The board"', 'string'],
        [', ', 'punctuation'],
        ['"Rules"', 'string'],
        [', … ],', 'punctuation'],
      ],
      [
        ['  "sections"', 'key'],
        [': ', 'punctuation'],
        ['38', 'number'],
        [',', 'punctuation'],
      ],
      [
        ['  "sinks"', 'key'],
        [': [', 'punctuation'],
        ['"board.ts"', 'string'],
        [', ', 'punctuation'],
        ['"store.sql"', 'string'],
        [', … ]', 'punctuation'],
      ],
      [['}', 'punctuation']],
    ],
  },
  {
    command: 'loom eject',
    colour: '#CE3F5C',
    wash: 'rgba(206, 63, 92, 0.12)',
    says: 'Loom does not lock you in. There is always a way out: the corpus is archived, and the tangled source is left running.',
    drawing: ejectDrawing,
  },
]

The block of JSON the weave draws

The weave's picture is not a tree but the document it produces, so it is drawn as JSON with each token in its own colour. The tokens carry a class rather than a colour, since a key is a key wherever it appears and the stylesheet is the right place to say what a key looks like.

TypeScript
const jsonLine = (line: ReadonlyArray<readonly [string, string]>, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('json-line')],
    Array.map(line, ([text, kind]) => h.span([h.Class(`tok-${kind}`)], [text])),
  )

const jsonBlock = (
  lines: ReadonlyArray<ReadonlyArray<readonly [string, string]>>,
  h: HtmlBuilder<Message>,
): Html =>
  h.div(
    [h.Class('px-json')],
    Array.map(lines, (line) => jsonLine(line, h)),
  )

A command's card

The card is the pill, the illustration in its frame, and the sentence. Two of the four read the clock as well, since their illustration moves. The frame holds a minimum height so four cards of different content still line up, and the illustration centres inside it.

TypeScript
const illustration = (operation: Operation, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('ops-frame')],
    [
      Match.value(operation.drawing).pipe(
        Match.when(Match.undefined, () =>
          h.div([h.Class('ops-art')], [jsonBlock(operation.json ?? [], h)]),
        ),
        Match.orElse((drawing) => h.div([h.Class('ops-svg'), h.InnerHTML(drawing)], [])),
      ),
    ],
  )

const operationCard = (operation: Operation, copied: string, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('ops-card')],
    [
      h.div(
        [h.Class('ops-head')],
        [
          h.button(
            [
              h.Class('ops-pill'),
              h.Type('button'),
              h.Title('Copy command'),
              h.Style({ color: operation.colour, background: operation.wash }),
              h.OnClick(Copied({ id: operation.command, text: operation.command })),
            ],
            [operation.command],
          ),
          h.span(
            [
              h.Class(copied === operation.command ? 'ops-took shown' : 'ops-took'),
              h.Style({ color: operation.colour }),
            ],
            ['copied'],
          ),
        ],
      ),
      illustration(operation, h),
      h.p([h.Class('ops-says')], [operation.says]),
    ],
  )

The section

The four cards sit in a grid of columns three hundred pixels wide, up to three of them, so the fourth wraps under the first and the row of commands reads as a sequence rather than a menu. A window too narrow for three columns at that width takes two, and below nine hundred pixels the grid becomes a rail to swipe rather than a column of cards each wider than what it holds.

TypeScript
export const fromInitToEject = (model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('section'), h.Id('loom-operations')],
    [
      h.div(
        [h.Class('section-head')],
        [
          carpetMark(operationsCarpet, '42px', broadSeam),
          h.div(
            [h.Class('lines')],
            [h.span([h.Class('eyebrow')], ['Loom CLI']), h.h2([], ['From init to eject'])],
          ),
        ],
      ),
      h.div(
        [h.Class('ops-grid')],
        Array.map(operations, (operation) => operationCard(operation, model.copied, h)),
      ),
    ],
  )

The module

The module is the four commands as data, the block of JSON the weave draws, a command's card, and the section that holds all four.

Loom