Docs

Document programs

Connect a document to a program on your computer and show its results on the page.

Type milk into a search box and press Search. Ripgrep finds the matching line in todo.htmlclay. The page shows the result below your list.

The page sends a request through HTML Clay. HTML Clay starts your program and passes the request on standard input. The program searches the file and returns JSON. Your page reads the JSON and displays it. HTML Clay calls this connection the wire.

There are two ways to attach a program. Use the terminal while trying a script. Let the document name its program when you want it to run without an open terminal. HTML Clay asks you to allow that program when you double click the file.

Before you start

You need HTML Clay installed and a file that opens through it. Get started covers that setup. The shell example below is for macOS and Linux. Install ripgrep and jq first. Check them in your login shell, whose PATH HTML Clay uses for document programs:

Terminal
$SHELL -lc 'rg --version && jq --version'

The command prints a version line for each tool.

On Linux, document permission prompts also need zenity or kdialog. On Windows, a document program must be a .exe, .bat or .cmd file.

Only connect a program you trust with your account. It runs as you and can read or change any file you can access.

Attach from the terminal

This example searches the saved HTML of a small todo list. Searching does not change the file.

Save this page as todo.htmlclay in a folder inside your home folder, such as ~/Documents/todo:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Todo</title>
</head>
<body>
  <h1>Todo</h1>
  <ul>
    <li class="todo" data-done="false">Buy milk</li>
    <li class="todo" data-done="false">Book dentist</li>
  </ul>
  <script src="https://clayjs.com/v1/clay.js?plugins=wire"></script>
  <script>
    clay.ready.then(() => {
      const panel = document.createElement("section");
      panel.setAttribute("clay", "no-save no-snapshot no-watch");
      panel.innerHTML = `
        <form>
          <label>Search the saved HTML <input name="query" required></label>
          <button>Search</button>
        </form>
        <pre aria-live="polite"></pre>
      `;
      document.body.append(panel);
      const form = panel.querySelector("form");
      const output = panel.querySelector("pre");
      const button = panel.querySelector("button");

      form.addEventListener("submit", async (event) => {
        event.preventDefault();
        const query = form.elements.query.value.trim();
        if (!query || button.disabled) return;
        button.disabled = true;
        output.textContent = "Searching";
        const request = clay.wire.send({ query }, { document: "none" });
        const outcome = await request.done;
        output.textContent = outcome.state === "done"
          ? outcome.result.matches.join("\n") || "No matches"
          : outcome.error || outcome.state;
        button.disabled = false;
      });
    });
  </script>
</body>
</html>

The wire plugin supplies clay.wire.send. The script builds the search controls each time the page opens. Their clay attribute keeps the controls and results out of saves. document: "none" skips saving the page before searching, so the search reads the saved file without any unsaved page edits.

Save these five lines as search.sh beside the document:

Terminal
#!/bin/sh
query=$(jq -er '.payload.query | select(type == "string" and length > 0)') || exit 1
matches=$(rg --no-heading --no-filename --color never -F -- "$query" "$HTMLCLAY_WIRE_FILE")
code=$?; [ "$code" -le 1 ] || exit "$code"
jq -cn --arg matches "$matches" '{type:"result",value:{matches:($matches | split("\n") | map(select(length > 0)))}}'

HTML Clay supplies the document's path in HTMLCLAY_WIRE_FILE. The program reads payload.query from the request. Ripgrep searches for that literal text and returns matching source lines. Exit code 1 means no matches, so the script returns an empty list. Other ripgrep failures stop the program.

Double click todo.htmlclay to open it. In your terminal, change to the folder containing both files. On macOS, first make the app's command available in that terminal:

Terminal
alias htmlclay="/Applications/HTMLClay.app/Contents/MacOS/htmlclay"

Skip the alias on Linux. Make the script executable, then attach it:

Terminal
chmod +x search.sh
htmlclay wire serve todo.htmlclay --protocol=jsonl -- ./search.sh

Leave that terminal running. HTML Clay starts search.sh once per request. --protocol=jsonl tells HTML Clay to read one JSON object per output line. Without that flag, output lines are plain progress text and no JSON result reaches the page.

Type milk and press Search. With the file exactly as shown, the page displays:

Output
    <li class="todo" data-done="false">Buy milk</li>

You are seeing a line from the saved file, including its HTML tags. Search for carrots and the page displays No matches.

Let the document name its program

The same search can run without an open terminal. The document names a program, and you choose which executable that name refers to.

Stop wire serve with Ctrl+C. Add this program name near the start of the document's <head>:

HTML
<meta name="htmlclay-helper" content="todo-search">

Replace the page's clay.wire.send line with this named request:

JavaScript
const request = clay.wire.send({ query }, { helper: "todo-search" });

In code and error text, HTML Clay calls a document program a helper.

Save the file and open it directly again by double clicking it. HTML Clay asks "Allow document programs?" before the page's script runs. On macOS and on Linux with zenity, the prompt offers two allow buttons:

  1. "Allow for This Document" remembers permission for this document, including after a restart.
  2. "Allow for Any Document" lets every document declaring that name use the selected program.

On macOS, "Deny" is the default button. Return and Escape also refuse access. A refusal lasts until you close the page and is never stored. Double clicking the file again asks again.

Choose "Allow for This Document". With kdialog, choose "Yes". If todo-search has no registered program, the picker opens with "Choose the program for todo-search". Select search.sh. The page then opens.

Search for milk again. HTML Clay starts the selected program and returns the same result.

The tray's Programs menu lists your registered programs. Use it to add a program or manage an existing program's access.

Read the result on the page

The result becomes available when request.done resolves. For the search above, the program prints this record:

JSON
{"type":"result","value":{"matches":["    <li class=\"todo\" data-done=\"false\">Buy milk</li>"]}}

The record's value becomes outcome.result. Check outcome.state before reading it. The done promise resolves for errors and cancellations too; it never rejects.

Named requests use document: "none" by default, so both attachment methods search the saved file.

A program can also edit the document itself. Use document: "edit" for that request and load the sync plugin alongside wire. ClayJS saves the page before sending the request and holds autosave until the request ends. Agent editing has a complete example. Live updates explains how file changes reach the open page.

Access and limits

clay.wire.send sends to the page's own file. A trusted folder lets HTML Clay open every .htmlclay file in it for editing. Any other file in that folder can reach this file's approved program.

Following a link to a file in a trusted folder does not set up its programs. Double click the file to set up its programs.

document: "none" only skips saving the page before the request. It does not stop a program from writing files.

Each file can have one attached program at a time. Stop a terminal attachment before switching to document programs.

Programs started with --protocol=jsonl have five minutes to finish, regardless of progress. A result record can contain at most 512 KiB, including its JSON wrapper. Keep answers small or divide them into separate requests.

If the search does not run

  1. "open it first" or "has never served this file's folder" in the terminal: open todo.htmlclay through HTML Clay, then attach again.
  2. "no agent is attached to this file" on the page: check that wire serve is still running. For a document program, check for a missing or misspelled <meta name="htmlclay-helper"> tag.
  3. "was not granted that helper" on the page: check the program declaration. Double click the file to be asked again.
  4. The page stays on "Searching" and the Search button stays disabled: stop wire serve, add --protocol=jsonl, restart the command and reload the page. Reserve the program's standard output for JSON records. Send diagnostic text to standard error.

For request states, cancellation and the complete output format, read the wire protocol reference. To read or change selected document content from a local program without a page request, use the JSON API.