Talk in Morse (mors) new

The mors icon: a cyan dot, dash and dot on a tape with Doto, a small violet dot, at the end of the row, and a magenta dash and dot below as Claude's reply.

One key, one tape. Key your message in dots and dashes and send it to Claude, who reads it as a prompt and does what it says. The window assumes you know Morse: only Doto helps you craft the message. mors lives in the store: it is not a default.

What it is

You hold a key. A short press is a dot, a long one a dash, and the tape shows them as you key. When the message is done, Send to Claude hands it over. Claude reads it as a prompt: it says in one line what it read, then does what the message asks, in the Claude Code transcript.

The window assumes you know Morse. By default it shows three things: the key, the tape and Send to Claude. No alphabet, no translation. Help comes from one place, Doto's menu: Read it back, Spell a word for me, Check what I keyed, Show the chart and Full controller (below).

Claude can talk Morse back. Its replies blink the lamp in magenta and sound a lower tone, on a second track of the tape under yours.

mors stores nothing on the host. The message lives in the window, and in a saved session after Send to Claude.

Install

mors is not in the engine's defaults.json. It lives in lets-store, so you install it once:

/lets store mors      # inspect → confirm
/lets mors            # open it

Claude shows you the inspection (permissions, problems, the verified flag) and copies it into ~/.lets/uniques/mors only after you say yes. Manual: clone the repo into ~/.lets/uniques/mors.

mors needs engine 0.4.0 or later ("engine": ">=0.4.0") for handoff sessions; it needs no new engine endpoint. It asks for no permissions: "fs.write": [], "net": []. Its capabilities are entity, chat and components.

This page describes mors 0.1.0 (2026-10-13). Like the other lets repositories, its repository is private for now.

What you can do

The tape and the key are the page: large, centred, calm, on the deep-violet dotted grid. A toolbar sits on top and a status line at the bottom.

PartYou seeYou can do
toolbarThe status dot, "mors", the wpm chip (12 wpm · auto) and Send to Claude; at the right end sound, chat, Doto and ?Read your speed on the chip: auto lights while the speed follows your hand. Press Send to Claude (Ctrl+Enter) when the message is done. There is no toolbar button for Doto's extras.
tapeYour track in cyan: dots are glowing discs, dashes glowing bars three times as long, words split by a faint /. The letter you are keying pulses at the end. Claude's track, in magenta, appears under yours after its first play. An empty tape says Key something. Hold Space, or the key.Read it. Each track scrolls to the end by itself. With read-back on, every letter shows its character above it, and a readout under the tape shows the decoded text.
key and lampA round knob on a base plate (120 px, 88 px on narrow screens) with a lamp ring. The lamp lights while you press, and in magenta while Claude plays.Hold it with the pointer, or hold Space. The tone sounds and the key squashes while you hold. A press shorter than 2 units is a dot, anything longer a dash.
hint lineUnder the key: Hold Space or the key · pause for a letter, longer for a word · Backspace erases a letter · ········ erases the wordRead it once. The full controller replaces it with its panel.
status lineAt the bottom: 3 words · 11 letters · 12 wpm · ghost: claude · spell · read-back off, and Doto is keying… while Claude playsRead it: the counts, the speed, whose ghost is on the tape and why, and whether read-back is on.

Keying. Pause after the last dot or dash and the letter closes; pause longer and the word closes. That is when Claude sees the message: settled words, never half a letter. With auto on, each press tunes the speed, so the window follows your hand (the rules are under the hood). If you prefer typing, . keys a dot, - or _ a dash, and / a word gap.

Erasing. Backspace removes the last dot or dash of the letter you are keying, or the last letter when none is open. Ctrl+Backspace removes the last word. Eight dots or more in one letter (........) is the operator's own eraser: it drops itself and the word you were keying, and the tape says erased for a second. Ctrl+Z undoes the last erase, insert or clear (50 steps).

A long message. A message holds up to 4,000 elements or 1,000 characters; keying beyond that is refused with a toast. The tape draws the last 300 letters, with a … n more marker at the left.

Mouse & keyboard

Outside text fields, unless the row says otherwise:

KeyDoes
Space (hold)The key
. - _A typed dot or dash
/A word gap (typed input)
Backspace / Ctrl+BackspaceErase the last element or letter / the last word
Tab / Shift+Tab (with a ghost)Key the ghost's next letter / the rest of it; without a ghost, Tab moves focus as usual
EscDismiss the ghost; then close the innermost open thing (chart, help, Say it, Ask)
Enter (Send armed by AR, SK or K)Send to Claude
Ctrl+EnterSend to Claude
Ctrl+ZUndo the last erase, insert or clear
MSound on / off
?The shortcuts overlay
Ctrl+/ · Ctrl+Space · right-clickThe shell, as in every unique: the chat · Doto

Everything is reachable with Tab in page order and every control has an accessible name. The key is a real button: Enter on it keys a dot, and Space on it is the key, as everywhere else. With read-back on, a screen reader hears the decoded text through the readout's live region; it hears a ghost once, when it appears ("Spell: HELLO"). Under reduced motion the lamp still lights (it is the signal), but the key does not squash and nothing pulses.

Doto's help

The extras are hidden by default because the window assumes you know Morse: you key and read dots and dashes, nothing else. Help is there when you ask for it, and only then. Doto's menu is the way in. Click him and pick:

In his menuWhat happens
Read it back / Hide the readingIn the window, no Claude needed. Each letter group shows its character above it, on both tracks, and a readout line under the tape shows the decoded text in a larger monospace. A group not in the table shows as #, with its raw pattern on hover.
Spell a word for meA one-line form opens under the toolbar: What do you want to say? Claude shows the letters as a ghost after the end of your message: dim cyan patterns, each with its character above, always readable even with read-back off. Key them. Each matched letter lights and the ghost shrinks; a word gap matches a space. Tab keys the next letter for you (lamp, tone, tape), Shift+Tab keys the rest. Esc or a letter that does not match drops it. Key it to the end and Doto winks.
Check what I keyedClaude reads your Morse and says in one Doto line what it read: "I read SHOW ME THE TESTS." If a group is off, it says which one and what it should be. It does not fix it unless you say so.
Show the chart / Hide the chartIn the window. The alphabet in a glass overlay: letters, digits, punctuation and prosigns. Each one is a button that keys it when the full controller is on; otherwise the chart is read-only. Esc closes it.
Full controller / Just the keyIn the window. A panel replaces the hint line (a bottom drawer on short screens): the readout, a Say it box (type words, Enter keys them at the current speed), the letter grid (each button shows the character and its pattern), Undo letter, Clear, the speed slider (3 to 30 wpm) with auto, sound, Chart, and an Ask box for a free question. Claude answers that in the chat, and in Morse on the tape when it fits.

The menu ends with Open chat. Each of the three toggles (read-back, chart, full controller) is remembered for the next window. Claude can open them too, with mors.reveal, when you ask in the chat ("show me the text"). It does not push them on you: it offers one when you seem stuck, such as three unreadable groups in a row, an erase storm or a question in the chat.

Your hand, your message. Claude never erases or inserts unless you ask, and it prefers a ghost over an insert: the point is that you key it. A ghost is an offer; a ghost you key away from is a "no", and Claude does not send the same one again. While you key, Claude stays quiet: the tape is your draft, not a conversation. Doto speaks on a sign-off, on an SOS (once), on a spell, a check or a finished play, never on a letter.

His reactions. At most one per thing you do: a wink when you key a ghost to the end, an oops on the eight-dot eraser, a cheer on Send to Claude, a nod when Claude's play finishes, and a start when a word closes as SOS.

Claude talks back

mors.play is Claude talking Morse. The lamp blinks magenta, a lower tone sounds, and the symbols appear on Claude's track under yours as they play (with their characters when read-back is on). The status line says Doto is keying… until the play ends with mors.played, and Doto nods. Plays queue in order. The key stays live: press during a play and you key as usual; the two tones differ in pitch.

Claude keeps plays short, 20 characters at most: R (received), K (over, your turn), 73 (best regards), SK (end of contact), or a word you asked it to spell when you also want to hear it. Whole answers go to the chat or the transcript, never to the tape.

The reply track is part of the window's state: reply holds what Claude has played so far, it travels with Send to Claude and it comes back on resume.

Send to Claude

Send to Claude (the button, or Ctrl+Enter) is a handoff. The window sends mors.export: the Morse, the window's decode, the counts, the speed, which extras were open, Claude's reply track, the end sign and the stats. The engine saves it as a session and closes the window, with a cheer from Doto. An empty message cannot be sent.

Then Claude:

  1. decodes the Morse itself. The window's text is a straight table lookup; your hand is not. Claude fixes what timing broke: a missing word gap that glued two words (HELLOWORLD), an extra one that split a word, a long dot read as a dash (E as T, S as U), a # that is two letters run together;
  2. says in one line what it read: "Read: SHOW ME THE FAILING TESTS." When its reading differs from the window's, it says so and goes with its own. An unreadable word gets a best guess and a question in the same line;
  3. treats the text as the prompt, exactly as if you had typed it in Claude Code: a question gets the answer, a task gets done, a request for a file gets the file. A greeting or a ham call (CQ, HELLO, TEST, SOS) gets a short greeting back; 73 or SK means you are leaving: one line, no questions;
  4. labels the session in three to six words of the message.

End signs. When a letter closes as <AR> (.-.-.) or <SK> (...-.-), or a word closes as K, the Send button glows and takes the next Enter. Nothing is sent by itself. The sign travels in the export as ends, and Claude reads it: AR is end of message (the normal case), K is over (you expect a reply and may come back, so Claude ends its answer with what you can key next), SK is end of contact (Claude closes the loop).

Resume. The window names the session after the decoded text (up to 60 characters) plus · mors, such as SHOW ME THE TESTS · mors. Say continue and the window comes back with mors.load: your message and Claude's reply track intact, with no ghost or accept events. If you signed off with K, Claude plays a short reply then (R K). If the tab also kept a draft, a load in the first 8 seconds wins over it.

With "handoff": "stay" in ~/.lets/config.json the window stays open after Send: Claude answers in the chat and plays R on the tape so you know it landed. If Claude is not listening, Send still exports, and the window says Doto is away (at most once every 30 seconds).

The manifest's session block names the roles:

"session": { "export": "mors.export", "snapshot": "mors.snapshot", "restore": "mors.load" }

Events (up)

Each event says what you meant. morse uses . and - only, letters split by a space and words by / : ... --- ... / -.-. --.- is SOS CQ. text is the window's decode, uppercase, one character per letter group; a group not in the table reads # and its raw pattern stays in morse. Prosigns read <AR>, <SK> and <SOS>.

EventWhen
mors.changed {rev, morse, text, letters, words, wpm, unit, view, by, delta?}A word closed (a word gap of silence after the last element), you erased or cleared, or a command from Claude or a restore changed the message. Settled only: never while a letter is open. by is "human", "claude" or "restore". delta {morse, text} is what was added since the previous mors.changed, when it was an append.
mors.ghost.accept {rev, text, assisted}You keyed the whole ghost, by hand or with Tab: assisted counts the letters the window keyed for you.
mors.ghost.reject {rev, reason, keyed}You pressed Esc ("esc"), keyed a letter that is not the ghost's next one ("keyed-away") or cleared the message ("cleared"). keyed is how many letters of the ghost had matched.
mors.ask {kind, question, morse, text, rev, view, source}You asked Doto for help: Spell a word for me (kind: "spell", question is the words you typed), Check what I keyed ("check"), or a free question in the full controller's Ask box ("free"). source is "menu" for spell and check, "form" for the Ask box.
mors.mode {view, by}You opened or closed one of Doto's extras (the full controller, the read-back or the chart), or Claude did with mors.reveal (by: "claude"). view is {controller: "key" | "full", readback, chart}.
mors.played {id, morse, ms}Nothing you did: a mors.play finished (id is the play's id).
mors.export {label, morse, text, letters, words, wpm, view, reply, ends?, stats}You pressed Send to Claude: the handoff. ends is "AR", "SK" or "K" when the message ends with that sign. label is the decoded text (up to 60 characters) plus · mors. The engine prepends session {id, handoff, saved, summary}.
mors.snapshotNothing you did: it is the reply to Claude's mors.snapshot command (re is the command's id), the export payload.

rev starts at 1 on an empty tape and never goes down; every change bumps it, a load included. stats counts dots, dashes, letters, words, errors, typed and assisted letters, and accepted and rejected ghosts.

Commands (down)

Commands are idempotent and animate (300 ms at most, transform and opacity only), except a play, which lasts as long as its message. Every command accepts "ack": true on the envelope and gets lip.ack {ok, error?} back. A bad command without ack gets lip.error {code: "bad_request", message, re}.

CommandDoes
mors.suggest {rev, text, reason?}Show text as the ghost after the end of the message: dim cyan patterns with the character above each. Up to 60 characters (letters, digits, punctuation, spaces), uppercased; characters not in the table are dropped. It shows when rev is the current one, or when the decoded text is still what it was at that rev; otherwise it is dropped silently. text: "" clears. reason shows in the status line (ghost: claude · spell). A new ghost replaces the old one without a reject event.
mors.play {id?, text?, morse?, wpm?, silent?}Play text (encoded) or morse on the reply track: the lamp blinks magenta and the lower tone sounds unless silent or sound is off. wpm defaults to the window's. Plays queue in order; {text: ""} stops and empties the queue. Ends with mors.played.
mors.insert {text?, morse?, play?}Append to your message as by: "claude". With play (the default) the window keys it at the current speed with lamp and tone, so you watch it go in; play: false appends at once. Bumps rev and emits mors.changed. Undoable. Claude asks before inserting more than a word: the message is your voice.
mors.erase {letters?, words?, all?}Remove from the end: letters: n, words: n, or everything. Undoable.
mors.reveal {controller?, readback?, chart?}Doto's extras from Claude's side: controller is "key" or "full", plus the read-back and the chart. Only the keys given change. Emits mors.mode {by: "claude"}.
mors.set {wpm?, unit?, auto?, sound?}Speed (wpm 3 to 30, or unit 40 to 400 ms), whether the unit keeps following your hand (auto), and the tone on or off.
mors.load {morse, text?, wpm?, view?, reply?, ghost?}The restore command: render the saved message and Claude's reply track without ghost or accept events, bump rev, and emit one mors.changed {by: "restore"}. In the first 8 s after boot it wins over the tab's draft. text is ignored: the window decodes morse itself. morse and reply keep the message limits; ghost is {text, keyed?} with the limits of mors.suggest.
mors.snapshot {}Reply with a mors.snapshot event.

A round trip: you pick Spell a word for me → mors.ask {kind: "spell", question} → mors.suggest {rev, text, reason: "spell"} → you key the ghost (or Tab) → mors.ghost.accept.

// spell two words as a ghost
{"type":"mors.suggest","data":{"rev":7,"text":"HELLO WORLD","reason":"spell"}}
// talk back: received, over
{"type":"mors.play","data":{"id":"p1","text":"R K"}}
// the same, as elements
{"type":"mors.play","data":{"morse":".-. / -.-"}}
// show the read-back, because you asked in the chat
{"type":"mors.reveal","data":{"readback":true}}
// slower, and stop following the hand
{"type":"mors.set","data":{"wpm":10,"auto":false}}

Under the hood

Timing

The table

International Morse: A–Z, 0–9, the punctuation . , ? ' ! / ( ) & : ; = + - _ " $ @ and the prosigns <AR> (.-.-.), <SK> (...-.-) and <SOS> (...---...). -...- decodes as =, -.--. as ( and .-... as &. A letter is a group of 1 to 8 elements; only the eraser, a run of dots, grows longer.

Sound and lamp

Web Audio, started on your first press (browsers want a gesture first). One sine oscillator per track: 700 Hz for you, 520 Hz for Claude, with 5 ms ramps so there is no click. Sound is on by default; M, the toolbar button, the full controller or mors.set {sound} turns it off, and the choice is remembered. The lamp always works, sound or not.

Storage and offline

Nothing is stored on the host. sessionStorage["lets.mors.draft"] keeps the message and the reply track for this tab, so a reload keeps them. localStorage["lets.mors"] holds preferences: sound, wpm, auto and view. Nothing else is stored.

Without a host, mors still works: keying, read-back, the chart and the full controller are all local. Send, Ask and the ghost need the host and say so with a toast.

The files

lets-unique-mors/
  unique.json            # manifest: domain mors, engine >=0.4.0, no permissions
  GUIDE.md               # what Claude reads on open
  CONTRACT.md            # the unique's behaviour spec
  README.md  CHANGELOG.md  LICENSE  icon.svg
  package.json          # npm run validate
  scripts/validate.mjs   # zero-dependency manifest check
  schemas/               # unique.schema.json and message schemas
  ui/index.html          # the page
  ui/mors.js             # the window
  ui/morse.js            # table, encode, decode, the keyer state machine, timing
  ui/style.css

Plain HTML, ES modules and CSS: no build step, no CDN, no inline script, no eval, no user content through innerHTML. MIT licensed. More: lets-unique-mors has the full GUIDE.md, the behaviour contract (CONTRACT.md) and JSON Schemas for the messages.

Develop

Run the host standalone from a lets checkout. The registry looks for <dir>/mors/unique.json, so point it at the parent of a folder named mors (a symlink or junction to this repo works):

LETS_UNIQUES_DIRS=/path/to/parent node engine/bin/lets.mjs serve --open mors --verbose

node engine/bin/lets.mjs send mors.play '{"text":"R K"}'   # drive it from the CLI
node engine/bin/lets.mjs events                            # drain what mors emitted

--verbose prints every event the window emits. Validate with npm run validate in the repo, or node engine/bin/lets.mjs validate <this folder> from the engine. The general recipe is in Develop & validate.