Driving textweaver from another program (JSON-RPC)
tw serve --stdio runs textweaver as a server controlled by another program. That program, the client, starts tw serve as a child process, writes requests to its standard input, and reads responses from its standard output. Through the protocol a client can open a document, navigate it by sentence, heading, line, or any other textweaver unit, have it read aloud with textweaver's voices, invoke any keyboard action by name, and follow playback word by word. This reference is intended for developers of editor plugins, web front ends, and other tools that want textweaver's reading engine without reimplementing it. The protocol is JSON-RPC 2.0, and the aspects that matter here are described where they arise.
Each message in the examples appears in its own code block, preceded by a sentence identifying it.
Before you start
twmust be installed;tw --versionconfirms it, and the install guide covers installation.- The protocol is JSON-RPC 2.0, and every message is a single JSON object. No JSON-RPC library is required: any language that can start a process and write and read lines is sufficient.
- Positions are zero-based character offsets from the start of the document, counted in Unicode characters (code points), as Python counts string indexes. JavaScript and VS Code count UTF-16 code units, so clients written for them must convert.
- Every example was produced by running the real
twprogram with--no-speech, so nothing was spoken aloud.
Start the server
This starts the server and speaks, like the terminal reader:
tw serve --stdio
The server prints nothing intended for a person. It waits for JSON messages on standard input and writes only JSON messages on standard output; it is meant to be started by a program, not typed at.
--stdio is required. Without it, tw serve stops at once with an error and exit code 1:
tw serve
This produces "Error: tw serve needs a transport; use --stdio". There is no network or socket mode; a client that needs a long-running server keeps the child process open.
Stay silent with --no-speech
tw serve --stdio --no-speech
Nothing is spoken, but the client still receives every announcement and playback change as a message and can display them or forward them to a screen reader. Use this option when the client has a voice of its own, as an editor used with a screen reader does. See Speech: who does the talking.
Choose a speech engine with --backend
tw serve --stdio --backend sapi
The value is a speech engine id, such as eci (ETI-Eloquence), sapi, dectalk, or null. The engines available on the current machine are listed by:
tw backends
Without --backend, the server uses the [speech] backend setting, normally auto. If the named engine is unavailable, the server falls back to the automatic choice and informs the client with the announcement "Speech engine sapi is not available; using …", naming the engine it selected. --backend has no effect together with --no-speech.
Keep settings in another folder with --home
tw serve --stdio --home "C:\textweaver-test"
Settings, reading positions, bookmarks, and notes are read from and saved under this folder instead of the usual locations, so that testing a client never alters real reading positions. Setting the TEXTWEAVER_HOME environment variable to a folder has the same effect.
Open a document at start with --open
tw serve --stdio --open notes.md
The document is open before the first request arrives, and the client receives "Opened" and the title as an announcement. If it cannot be opened, the server keeps running with no document and sends an announcement beginning "Could not open". A document can always be opened later with the open method.
How messages travel
One message per line
The simplest framing is one JSON object per line, terminated by a newline. The server responds in the same way, and blank lines are ignored.
Content-Length framing, like the Language Server Protocol
A client may instead put a header before each message, the way language servers do:
Content-Length: 42
{"jsonrpc":"2.0","id":2,"method":"status"}
The header line ends with a carriage return and line feed, and a blank line (another carriage return and line feed) precedes the message. The length is the size of the message in bytes, in UTF-8, and the header name is case-insensitive. Content-Length must come first: header lines after it, such as Content-Type, are skipped, but a line before it is read as a message of its own and fails.
The server determines the framing of each incoming message: a line beginning with Content-Length: introduces a framed message, and any other line is a complete message. It responds, and sends its own notifications, in the framing of the last message received; before the first message it uses one message per line. Framed responses carry only a Content-Length header.
A client should choose one framing and keep to it. A framed response is not followed by a newline, so switching in mid-session can place the next line-delimited response on the same line.
What every message looks like
Every message is a JSON-RPC 2.0 object:
jsonrpcmust be the string"2.0". A message without it is refused.methodis the name of the method, such asopen.paramsis an object with the method's parameters. Leave it out when a method has none.idis a number or string chosen by the client. The response carries the sameidso that the two can be matched. A message without anidis a notification: the server executes it but sends no response.
A response contains either result (success) or error (failure), never both. Requests are answered in the order received.
A message may also be a JSON array of requests, a batch. The server handles the requests in order but sends the responses individually, as separate messages rather than as one array.
Server messages that have method and no id are notifications. They may arrive at any time: before the response to the request that caused them, and between requests while speech continues. A client must therefore be prepared to read them whenever it reads.
Speech: who does the talking
Without --no-speech, the server speaks exactly as the terminal reader does, with the same settings, engine selection, and voices. It reads the document aloud and also speaks its own announcements, such as "Opened Cell biology." The client never receives audio.
Every announcement is also sent to the client as an announcement notification carrying its text and priority. A client therefore has two modes of operation:
- Let textweaver speak. The client displays the announcements or ignores them.
- Start the server with
--no-speech. The client receives the announcements and passes them to its own voice or to the user's screen reader. This is the appropriate choice for screen reader users, who would otherwise hear two voices at once.
The server does not follow the reader's accessibility mode ([accessibility] mode; see Using textweaver with a screen reader). It always speaks for itself unless started with --no-speech, so a client gets the mode it chose regardless of how the terminal reader was last configured.
With --no-speech, reading takes no time. A request to read yields "reading", one position notification, and "stopped" almost immediately.
Methods
The server has 29 methods, all listed by initialize. Parameters marked optional may be omitted.
initialize: say hello and learn the version
Parameters: none.
The result is an object with:
server: always"textweaver".version: the textweaver version, such as"0.1.0-beta.1".protocol: the protocol version, a number, currently1.methods: the names of every method.notifications: the names of every notification.
Calling initialize first is not required; every method works without it.
open: open a document
Parameters:
path(string, required): what to open, in any formtw openaccepts: a file, a file inside an archive (book.zip!chapter.pdf), or a web address. A relative path is resolved against the folder in which the server was started.
The result is an object with:
document: facts about the document (see below).position: where reading starts, in the same shape as the result of thepositionmethod. If the document was read before, this is the saved position, and the announcement says "Resumed at" and a percentage.
The document object has:
title: the document's title. For the Markdown example below it is the first heading. For a plain text file it is the file name.path: the full path, or null.format: the format, such as"markdown".length: the number of characters in its text.lines: the number of lines.editing: true while edit mode is on.dirty: true when there are unsaved edits.
If edit mode is on when open is called, textweaver leaves edit mode first. The result then contains document and effects instead of document and position, so the client should call position afterwards.
Errors: -32602 when path is missing, and -32002 when the file does not exist, is a folder, or cannot be read.
status: everything at once
Parameters: none. It works with or without a document.
The result is an object with:
mode: the reader's mode, such as"Browse","Edit","Speech Cursor","Find","Go to", or"Command".playback:"reading","paused", or"stopped".document: the same objectopenreturns, or null when no document is open.position: the same objectpositionreturns, or null.rate: the reading rate in words per minute.backend: the speech engine id, or"silent"with--no-speech.status: the last status message, identical to the text on the terminal reader's status line. It is empty initially.pending: the yes-or-no question waiting for an answer, or null. See action.
position: where the cursor is
Parameters: none. It needs an open document.
The result is an object with:
char: the character offset of the reading position.line: the line number, starting at 1.column: the column, starting at 1.percent: how far through the document, from 0 to 100, rounded down.word: the word at the position, as{start, end, text}, or null when the position is not inside a word (on a space, for example).
navigate: move by a unit or go to a place
It needs an open document. Give one of these parameters:
action(string): the id of a movement action, such asnext_sentence,previous_paragraph,next_heading,skip_next_heading, ornext_bookmark. Actions from four groups of the keyboard reference are accepted: Navigation, Speech Cursor, Search, and Bookmarks and notes.goto(string): a destination. It accepts a line number ("12"or"line 12"), a percentage ("50%"or"50 percent"), a character offset ("char 120"),"start"(also"top"or"beginning"), or"end"(also"bottom"). Headings ("heading 2") are not accepted here.
When both are given, goto is used.
The result is the new position, in the same shape as the result of the position method. The move is also announced, usually by speaking the text at the destination.
Errors: -32602 "Not a navigation action" for an action from another group, and "Not a go-to target" for a goto value it cannot understand.
Some actions in these groups open a prompt or a list, for example find and list_bookmarks. Through navigate, the client receives only the announcement; for these, use the action method, which returns the prompt or list in its result.
read: read aloud
It needs an open document. Parameters:
what(string, optional): what to read. The default iscursor.cursor: read on from the cursor until you stop it.document: read from the start of the document.paragraph: read on from the start of the current paragraph (thereplay_paragraphaction).sentence,line,word,character: read just that one unit at the cursor, without moving.selection: read the selected text. With nothing selected you hear "No selection."from(number, optional): a character offset to move to before reading.
The result is {playback}, the playback state immediately after the request: "reading", "paused", or "stopped". For a short unit it may already be "stopped". playback and position notifications follow as reading proceeds.
Errors: -32602 "Cannot read" and the word you gave, for anything not in the list above.
pause, resume, and stop
Parameters: none. The result of each is {playback}.
pausepauses reading and has no effect unless reading is in progress.resumecontinues after a pause and has no effect unless reading is paused.stopstops reading.
pause and resume need an open document. stop works without one.
search: find text
It needs an open document. Parameters:
pattern(string, required): the text to find.regex(true or false, optional): treat the pattern as a regular expression. The default is false.
Matching follows the same rules as Find in the reader; in testing, a lowercase pattern also matched a capitalized word.
The result is an object with:
matches: every match, each{start, end, line}(character offsets and a line number starting at 1).current: the index inmatchesof the match the cursor moved to, or null.
The cursor moves to the first match at or after its position, wrapping to the top when necessary, and the announcement is, for example, "Match 1 of 2: The nucleus".
text: get the document's text
It needs an open document. Parameters:
start(number, optional): the first character offset. The default is 0.end(number, optional): the offset just after the last character. The default is the end of the document.
Offsets past the end are treated as the end, and an end before start yields empty text. The result is {text, start, end}. The text is the canonical text that tw text prints and that textweaver reads aloud.
outline, notes, highlights, and info: read what the reader knows
These four methods are read-only: they change nothing and speak nothing. Each requires an open document and takes no parameters. Offsets are characters in the canonical text, as for text, and line counts from 1.
outlinereturns{items, pages}. Each item is{level, text, char, line}: a heading, its level from 1 to 6, its text on one line, and where it starts. In a paged document without headings, such as a PDF, the items are its pages, each with level 0, andpagesis true.notesreturns{notes}, in document order. Each note is{id, start, end, line, anchor, text, tags, color}:anchoris the noted text,textis what the user wrote,tagsthe words written as#tag, andcolora color name or null.highlightsreturns{highlights}, in document order. Each is{id, start, end, line, color, text}, wheretextis the highlighted text andcolora color name such as"yellow".inforeturns facts about the document:title,path,format,author,language(each may be null), and the countschars,words,lines,headings,notes,highlights, andbookmarks.reading_minutesis the time to read it aloud atrate, the speech rate in words per minute.
For example, the outline of the document in the example session:
{"jsonrpc": "2.0", "id": 9, "method": "outline"}
{
"id": 9,
"jsonrpc": "2.0",
"result": {
"items": [
{"char": 0, "level": 1, "line": 1, "text": "Cell biology"},
{"char": 90, "level": 2, "line": 5, "text": "The nucleus"}
],
"pages": false
}
}
And in Python, with the call function from the minimal client, a study list of every note:
for note in call("notes")["notes"]:
print(f"Line {note['line']}: {note['anchor']}: {note['text']}")
Errors: -32001 when no document is open.
insert: type text at the cursor in edit mode
It requires an open document and edit mode. This is the means by which a client, such as a dictation front end, enters text as though typed at the keyboard: the insertion is a single undo step and is echoed as typing is. Parameters:
text(string, required): the text to insert at the cursor.
The result is {status, effects, position}, the same as action, plus the new position.
Errors: -32602 when text is missing, and -32004 "insert needs edit mode; run the toggle_edit_mode action first" when edit mode is off. Turn on edit mode first with action and toggle_edit_mode.
action: run any command by name
This method lets a client perform anything a key can do. Parameters:
id(string, required): any action id from the keyboard reference, such asadd_bookmark,go_to,toggle_edit_mode,save, orquit. It can also be one of the notes commands:add_note,list_notes,next_note,previous_note,toggle_highlight,list_highlights,rename_bookmark,delete_bookmark, ornote_links.confirm(true or false, optional): the answer to give if the action asks a yes-or-no question first, asquitanddelete_notedo.trueanswers yes andfalseanswers no.
The result is an object with:
status: the status message after the action, the same text as its announcement.effects: what the action asked the client to show. Each effect is{type, params}:prompt, withparams{label, purpose}: a question that needs typed text, such as a Find or Go to box.labelis what to show or say.purposesays what the answer is for, in lowercase with underscores; the list of purposes is under Prompt purposes. Answer it withanswer.list, withparams{title, items}: a list to choose from, such as bookmarks or notes.itemsis a list of strings. Pick one withchoose.quit, with emptyparams: the app quit. The server stops after sending this answer.pending: when the action asked a yes-or-no question and you did not giveconfirm, this is{action, question}, for example{"action": "quit", "question": "Quit textweaver? y or n"}. Otherwise it is null. Answer later with anotheractioncall that hasconfirm, or withcancelfor no.
The notes commands return status and effects but no pending.
Prompt purposes
A prompt's purpose is one of these 31 purposes. The names are fixed: within protocol version 1 a purpose may be added but none is renamed or removed. A client should treat an unknown purpose as an ordinary text question, presenting the label and sending what the user types.
find: the text to find.go_to: a line, a percentage,start, orend.open: the document to open.command_palette: the name of a command to run.save_as: the file to save to; empty accepts the name in the label.export_as: the file to export the document to; empty accepts the name in the label, the document's own name and folder with the format's extension.table_size: a table size such as3 by 2(columns by rows).image_path: the path of an image to insert.replace_find: the text to replace (the replacement is asked next).replace_with: the replacement for every match.note_text: the text of a new note.edit_note: the new text of a note; empty keeps it.rename_bookmark: the new name of a bookmark; empty keeps it.export_settings: the file to export settings to.import_settings: the settings file to import.citation_locator: the page for a citation being inserted; empty inserts none.reference_identifier: a DOI or ISBN to look up and add.import_references: a BibTeX, RIS, or CSL-JSON file to import.template_title: the title of a new document from a template.define_word: a word to define.profile_name: the name of a new settings profile.rename_profile: the new name of a settings profile; empty keeps it.import_profiles: a profile export to import.export_profiles: the file to export the profiles to.setting_value: a new value for the setting chosen in the settings list; empty keeps it.sync_computer_name: this computer's name for sync; empty keeps the one suggested.document_details: one field of the document details form (title, author, DOI, or ISBN).prompt_keywithtabmoves between the fields, andanswersaves them all.comment_reply: a reply to the comment chosen in the changes list.comment_text: the text of a new comment on the selection or the sentence at the cursor.import_cards: an Anki package (.apkg) whose cards to add to the open document's cards.github_token: a GitHub token for a private components source; empty skips. It is secret:textin the prompt state is one star per character, and a client should mask what it shows and never log it.
The reader writes files in the background (saves, bookmarks, notes, positions). The server waits for those writes before responding, so when an action such as save or add_bookmark returns, the file is on disk.
Errors: -32602 "No action" and the id, for an id that does not exist, and "confirm must be true or false".
answer, choose, and cancel: reply to a prompt or list
answertakestext(string, required): the text typed into the open prompt. For a Go to prompt, for example,"5"goes to line 5.choosetakesindex(number, required): the item of the open list to pick, starting at 0.canceltakes no parameters. It closes the open prompt or list, or answers no to a waiting yes-or-no question. You hear "Cancelled."
Each returns {status, effects}, the same as action without pending.
list_state and list_key: move through a list as the reader does
textweaver itself keeps the displayed list and its focused item, identically for the terminal reader, the app, and a client. These methods let a client move through a list with the reader's own keys and receive the same "item, 2 of 5" announcements.
list_statetakes no parameters. It returns the list shown as{title, items, selected, filter}, or null when no list is shown.selectedcounts from 0;filteris the text typed so far in a list that filters as you type (the outline, the citation picker, the settings), else null.list_keytakeskey(string, required):up,down,page_up,page_down,home,end,left,right,enter,escape,backspace,delete,rename,introduce(the list's introduction again),details(the Say Status key: in the file browser, a preview of the focused row), the file browser'schoose_here,sort, andshow_all, or one character. A character filters a list that filters, chooses by a list's own letter (s,d,cin Save, Discard, Cancel), or moves to the next item starting with it; a space marks an item (a favorite voice).leftandrightchange a value in the settings list. It returns{status, effects, list}, wherelistis the list after the key, aslist_stategives it.
prompt_state and prompt_key: type into a prompt
prompt_statetakes no parameters. It returns the prompt open as{label, purpose, text, caret}, or null.caretcounts characters from the start oftext.prompt_keytakes eitherkey(string): one character, orbackspace,delete,left,right,home,end,upanddown(earlier answers to the same prompt, or command palette matches),tab(completes a command name or a file path, or moves to the next field of the edit details form),shift_tab(the form's previous field),kill_to_start,kill_to_end,delete_word_back,enter, orescape; ortext(string): the whole text of the prompt at once. It returns{status, effects, prompt}.
answer still works: it answers the open prompt with the text you give.
settings_schema, get_setting, and set_setting: change settings
settings_schematakes no parameters. It returns every setting insettings.toml, in order, as an array of objects withpath(such as"speech.rate"),section,label,help,kind,default, andinternal(true for the few that textweaver keeps for itself). Bykind:"toggle": on or off."number": withmin,max,step, andunit(such as"words per minute")."choice": withchoices, a list of{value, label}, andopen(true when other values may be typed, such as a theme of your own)."text": withoptional(true when it may be unset, with null)."list": a list of texts, such as library folders."table": names and values, such as pronunciations; edit these insettings.toml.get_settingtakespath(string, required). It returns{path, value, spoken}: the value as JSON, and as it reads aloud ("265 words per minute").set_settingtakespath(string, required) andvalue(any JSON; null puts the default back). The value is checked against the setting's type; a number outside its range is set to the nearest value in range. The change takes effect at once (the voice, the theme, the keys) and is saved before the answer. It returns{path, value, spoken}.
Errors: -32602 "There is no setting" and the path, or a sentence saying why the value does not fit.
shutdown and exit: finish
shutdowntakes no parameters and returns null. It saves the reading position and settings. After it, every request exceptexitfails with error-32003.exittakes no parameters. Send it as a notification (with noid). The server stops and closes its output. If you did not callshutdownfirst,exitsaves anyway. If you sendexitwith anid, you get a null result first.
Closing the server's standard input also stops it, and saves the position too. So a client that is killed or crashes does not lose the reader's place.
The server does not poll on a timer. The speech engine wakes it as each word is spoken, so position notifications are sent immediately; otherwise it checks for work about four times a second.
Notifications from the server
These arrive without an id:
announcement, with{text, priority}: something the app said, identical to what a screen reader user of the terminal reader would hear.priorityis"polite"or"assertive"; assertive announcements are urgent, such as errors, and should interrupt.playback, with{state}: reading started ("reading"), paused ("paused"), or ended ("stopped").position, with{start, end, line}: the word being spoken at that moment, as character offsets and a line number starting at 1, suitable for highlighting or scrolling. One is sent for each word the speech engine reports.
initialize also lists prompt, list, and quit as notifications. In this version the server does not send them independently; they arrive only inside the effects of the results of action, answer, choose, and cancel, as described under action.
Errors
An error response has this form:
{"error":{"code":-32001,"message":"No document is open; call open first"},"id":3,"jsonrpc":"2.0"}
The codes JSON-RPC defines:
-32700, parse error: the message is not JSON. The message starts "Parse error:" and theidis null.-32600, invalid request: the message is not a JSON-RPC 2.0 request, for examplejsonrpcis missing. The message is "Not a JSON-RPC 2.0 request". An empty batch ([]) gives "Empty batch".-32601, method not found: "No method" and the name you sent.-32602, invalid parameters: a parameter is missing or has the wrong type or value. The message says which, for example "path is required and must be a string" or "index must be a number".
The server's own codes:
-32001, no document: the method needs an open document. "No document is open; call open first".-32002, open failed: the document could not be opened. The message names the file and the reason.-32003, shut down: a request arrived aftershutdown. "The server is shutting down; send exit".-32004, not editing:insertwas called outside edit mode. "insert needs edit mode; run the toggle_edit_mode action first".
Versions
initialize reports protocol, currently 1. The design record, ADR-0015, sets the compatibility rule: adding methods, notifications, parameters, or result fields keeps version 1, so a client must ignore fields it does not recognize, while renaming or removing anything, or changing the meaning of anything, raises the version. A client should check protocol on connecting and refuse to continue if the version is unknown.
The server emits the fields of each object in alphabetical order; clients must not depend on field order.
A complete example session
This is a real session. The client started the server with this command:
tw serve --stdio --no-speech
The document was a small Markdown file, notes.md:
# Cell biology
Cells are the smallest units of life. Every living thing is made of cells.
## The nucleus
The nucleus holds the cell's DNA. It controls growth and division.
Each message was sent on one line. Responses are shown here with line breaks added for readability, and the actual folder of the file is replaced with C:\Users\you\Documents.
Say hello
The client asks for the server's version.
{"jsonrpc": "2.0", "id": 1, "method": "initialize"}
The server answers with its name, version, protocol, and the lists of methods and notifications.
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"methods": ["initialize", "open", "status", "position", "navigate", "read", "pause", "resume", "stop", "search", "text", "outline", "notes", "highlights", "info", "insert", "action", "answer", "choose", "cancel", "list_state", "list_key", "prompt_state", "prompt_key", "settings_schema", "get_setting", "set_setting", "shutdown", "exit"],
"notifications": ["position", "playback", "announcement", "prompt", "list", "quit"],
"protocol": 1,
"server": "textweaver",
"version": "0.1.0-beta.1"
}
}
Open the document
The client opens notes.md. Backslashes in a Windows path are doubled inside JSON.
{"jsonrpc": "2.0", "id": 2, "method": "open", "params": {"path": "C:\\Users\\you\\Documents\\notes.md"}}
First comes the announcement, as a notification.
{"jsonrpc": "2.0", "method": "announcement", "params": {"priority": "polite", "text": "Opened Cell biology."}}
Then the answer, with the document and the starting position, the word "Cell" at the very start.
{
"id": 2,
"jsonrpc": "2.0",
"result": {
"document": {
"dirty": false,
"editing": false,
"format": "markdown",
"length": 169,
"lines": 7,
"path": "C:\\Users\\you\\Documents\\notes.md",
"title": "Cell biology"
},
"position": {
"char": 0,
"column": 1,
"line": 1,
"percent": 0,
"word": {"end": 4, "start": 0, "text": "Cell"}
}
}
}
Move to the next sentence
The client moves by one sentence.
{"jsonrpc": "2.0", "id": 3, "method": "navigate", "params": {"action": "next_sentence"}}
The server announces the sentence it arrived at.
{"jsonrpc": "2.0", "method": "announcement", "params": {"priority": "polite", "text": "Cells are the smallest units of life."}}
The answer is the new position: line 3, character 14, the word "Cells".
{
"id": 3,
"jsonrpc": "2.0",
"result": {
"char": 14,
"column": 1,
"line": 3,
"percent": 8,
"word": {"end": 19, "start": 14, "text": "Cells"}
}
}
Read that sentence aloud
The client asks for the sentence at the cursor to be read.
{"jsonrpc": "2.0", "id": 4, "method": "read", "params": {"what": "sentence"}}
Reading starts.
{"jsonrpc": "2.0", "method": "playback", "params": {"state": "reading"}}
The answer says reading is going on.
{"id": 4, "jsonrpc": "2.0", "result": {"playback": "reading"}}
A position notification says which word is being spoken. With a real voice there is one for every word. With --no-speech there is only this one.
{"jsonrpc": "2.0", "method": "position", "params": {"end": 19, "line": 3, "start": 14}}
Reading ends.
{"jsonrpc": "2.0", "method": "playback", "params": {"state": "stopped"}}
Ask where the cursor is
The client asks for the position.
{"jsonrpc": "2.0", "id": 5, "method": "position"}
Reading one sentence did not move the cursor.
{
"id": 5,
"jsonrpc": "2.0",
"result": {
"char": 14,
"column": 1,
"line": 3,
"percent": 8,
"word": {"end": 19, "start": 14, "text": "Cells"}
}
}
Run an action: add a bookmark
The client runs the add_bookmark action, the same as pressing m in browse mode in the terminal reader.
{"jsonrpc": "2.0", "id": 6, "method": "action", "params": {"id": "add_bookmark"}}
The announcement.
{"jsonrpc": "2.0", "method": "announcement", "params": {"priority": "polite", "text": "Bookmark mark1 set at 8 percent."}}
The answer: no effects and no question waiting.
{"id": 6, "jsonrpc": "2.0", "result": {"effects": [], "pending": null, "status": "Bookmark mark1 set at 8 percent."}}
Run an action that asks for text: go to
The client runs go_to, which opens a prompt.
{"jsonrpc": "2.0", "id": 7, "method": "action", "params": {"id": "go_to"}}
The prompt's label is announced.
{"jsonrpc": "2.0", "method": "announcement", "params": {"priority": "polite", "text": "Go to line, percent, start, or end"}}
The answer carries the prompt as an effect.
{
"id": 7,
"jsonrpc": "2.0",
"result": {
"effects": [
{"params": {"label": "Go to line, percent, start, or end", "purpose": "go_to"}, "type": "prompt"}
],
"pending": null,
"status": "Go to line, percent, start, or end"
}
}
The client answers the prompt with line 5.
{"jsonrpc": "2.0", "id": 8, "method": "answer", "params": {"text": "5"}}
The server announces where it went.
{"jsonrpc": "2.0", "method": "announcement", "params": {"priority": "polite", "text": "53 percent, line 5: The nucleus"}}
The answer.
{"id": 8, "jsonrpc": "2.0", "result": {"effects": [], "status": "53 percent, line 5: The nucleus"}}
Finish
The client asks the server to save and get ready to stop.
{"jsonrpc": "2.0", "id": 9, "method": "shutdown"}
The answer is null.
{"id": 9, "jsonrpc": "2.0", "result": null}
The client sends exit as a notification. There is no answer. The server closes its output and ends with exit code 0.
{"jsonrpc": "2.0", "method": "exit"}
The next time this document is opened, the announcement is "Opened Cell biology. Resumed at" and the saved percentage.
A minimal client in Python
This client uses only Python's standard library. It starts the server without speech, opens the named file, moves to the next sentence, prints every announcement, and shuts down cleanly. It reads one line at a time and skips notifications until the response to its request arrives.
import json
import subprocess
import sys
# Start the server as a child process. Use "tw" if it is on your PATH.
server = subprocess.Popen(
["tw", "serve", "--stdio", "--no-speech"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
encoding="utf-8",
)
next_id = 0
def call(method, params=None):
"""Send one request and return its result, printing announcements."""
global next_id
next_id += 1
request = {"jsonrpc": "2.0", "id": next_id, "method": method}
if params is not None:
request["params"] = params
server.stdin.write(json.dumps(request) + "\n")
server.stdin.flush()
while True:
line = server.stdout.readline()
if not line:
raise RuntimeError("the server stopped")
message = json.loads(line)
if message.get("method") == "announcement":
print("Announcement:", message["params"]["text"])
if message.get("id") == next_id:
if "error" in message:
raise RuntimeError(message["error"]["message"])
return message["result"]
info = call("initialize")
print("Connected to", info["server"], info["version"], "protocol", info["protocol"])
opened = call("open", {"path": sys.argv[1]})
print("Opened", opened["document"]["title"], "with", opened["document"]["lines"], "lines")
position = call("navigate", {"action": "next_sentence"})
print("Now at line", position["line"], "column", position["column"])
call("shutdown")
server.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "exit"}) + "\n")
server.stdin.flush()
server.wait()
Save it as client.py and run it with a document:
python client.py notes.md
With the example document, opened for the first time, it prints:
Connected to textweaver 0.1.0-beta.1 protocol 1
Announcement: Opened Cell biology.
Opened Cell biology with 7 lines
Announcement: Cells are the smallest units of life.
Now at line 3 column 1
This client reads only while it waits for a response. A production client, such as an editor plugin, should read the server's output continuously, on its own thread or in an event loop, because position and playback notifications keep arriving while speech continues.
If something goes wrong
tw servestops at once with "needs a transport". Add--stdio.- Nothing comes back. Check that each message ends with a newline, or has a correct
Content-Lengthheader, and that your client flushes its output after writing. Check that the message has"jsonrpc": "2.0". A message without anidis a notification and never gets an answer. - Error -32001, "No document is open". Call
openfirst, or start the server with--open. - Error -32002 when opening. The path is wrong or the file cannot be read. A relative path is taken from the folder the server was started in, so send a full path when you can.
- Error -32602, "Not a navigation action".
navigatetakes only movement, Speech Cursor, search, and bookmark actions. Useactionfor everything else. - Error -32003. You already called
shutdown. Sendexitand start a new server. - The user hears two voices. Both textweaver and the user's screen reader are speaking. Start the server with
--no-speechand pass the announcements to the screen reader. See the screen reader guide. - A prompt or list opened, but the client was not told. Actions run through
navigateorreaddo not return their effects. Run them withaction, then useanswer,choose, orcancel.cancelalways closes whatever is open. - Highlighting is off by a few characters in JavaScript or VS Code. Positions count Unicode characters, and those tools count UTF-16 units. Convert before highlighting.
- No speech, and an announcement says the speech engine is not available. Run
tw backendsto see which engines work on this computer, and see the troubleshooting guide. - You need more detail. The server writes nothing but protocol messages to standard output, and nothing to standard error. Problems are written to the log file,
textweaver.log, in the state folder (with--home, that is thedata\statefolder inside it). Set theTEXTWEAVER_LOGenvironment variable todebugbefore starting the server to log more.
See also
- ADR-0015: JSON-RPC server: the design decision behind
tw serve. - Architecture: how the server shares the app core with the terminal reader and the app.
- Keyboard reference: every action id you can pass to
actionandnavigate. - Using textweaver with a screen reader:
--no-speechand working with JAWS, NVDA, VoiceOver, and Orca. - Troubleshooting: the log file and common problems.
- Documentation index