ctrl-kd
Convert WordStar-era files to modern formats. ^KD: save and done.
ctrl-kd reads WordStar 4 documents, WordStar 5–7 documents, and WordStar
print-to-disk files (the printer byte stream, captured to a file — a distinct
format most converters mangle), and writes plain text, Markdown, HTML, RTF, or PDF (typewriter-set on the
built-in Courier fonts — no dependencies, the page as it would have printed).
$ ctrl-kd ESSAY.WS # -> ESSAY.md
$ ctrl-kd ESSAY.WS -t html -t rtf # multiple formats
$ ctrl-kd ESSAY.WS -t pdf --mode printed # a facsimile of the 1990 printout
$ ctrl-kd --mode printed LETTER.WS # line-for-line, as it printed in 1990
$ ctrl-kd --diagnose MYSTERY.FIL # what IS this file?
$ ctrl-kd --comments MEMO.WS # include the author's hidden comments
$ ctrl-kd --no-notes PAPER.WS # body text only, no notes
Why another converter?
Existing tools each lose something. Fed a WordStar 4 file, converters written for
WS7 delete the last letter of every word (WS4 set bit 7 on it). Most delete soft
returns outright — Jon Michaels + March 6, 1992 becomes
Jon MichaelsMarch 6, 1992 — which also destroys every poem, because poem lines
end in soft returns too. And print-to-disk files aren't WordStar documents at all,
so feeding them to a WordStar converter produces stray superscripts and garbage.
ctrl-kd was built by converting a real 1987–1992 corpus (high-school and college
papers, poems, stories — WordStar 4 on DOS, dot-matrix printer) and verifying
against surviving period printouts of the same documents. Its rules are empirical:
- Detection by content, never by extension. WS4 vs WS5+ vs print stream vs
plain text vs binary, with the evidence shown in
--diagnose. - The wrap test. WordStar wrapped only when the next word didn't fit. So a soft return where the next word would have fit (strictly — WordStar wrapped even on an exact-margin fit) is a deliberate break: a poem line, a heading. Everything else is word wrap and joins with a space. The margin is estimated from the 90th percentile of soft-wrapped line lengths (floor 65, the default).
- Break runs. Soft/hard return runs containing a hard return and a blank line are paragraph breaks; a lone hard return is the author's deliberate line break. Double-spaced documents (blank soft lines between every line) collapse automatically.
- Ruler lines mean columns. A
.rr----!----dot line defines tab stops; the document's alignment is space-built and only survives fixed-width. Such documents renderprintedin every mode. - Print streams render verbatim — they ARE the printed page — with printer
style codes decoded (superscript/underline/italic/bold pairs; table in
core.PRINT_CODES, derived from a late-80s dot-matrix driver and overridable). - WS5+ symmetric blocks (
0x1D: real footnotes/endnotes, headings, page breaks — machinery added in WS5) are parsed with their nested structure, verified against the 86 WordStar 7 documents in Robert J. Sawyer's public WordStar archive. All four note kinds WordStar distinguished are read and kept apart — footnote, endnote, annotation, and comment — with in-text references ([^n]in Markdown, DPUB-ARIA anchors in HTML, real\footnotedestinations in RTF). Comments never appear unless you ask for them, since WordStar never printed them;--diagnosestill reports that they exist. Paragraph styles become headings, and 82/86 convert with zero mojibake. More WS5–7 corpora still welcome. - Page geometry from the file's own
.pl/.po/.mt/.mb/.hm/.fm/.lh/.ls/.cw, so--mode printedreproduces WordStar's own page: the vertical model (.pl − .mt − .mbat the.lhline height — 55 text lines for WordStar's defaults, not a guessed 1-inch margin), the horizontal one (.popage offset at the.cwcharacter pitch), and WordStar's own line breaks — a soft return is where the line broke on paper, so printed output keeps it (and reflowed Modern output still joins it).--diagnosesays whether each figure came from the file or from the default. Inprintedmode footnotes are laid out the way WordStar laid them out: at the foot of the page that references them, behind a twenty-dash separator, split across pages with...Continued...when they do not fit.
Modes
--mode modern(default): reflowed paragraphs, semantic markup, deliberate line breaks kept.--mode printed: every line as laid out, fixed-width,.pa/form-feed page breaks honored — how it came off the printer.
Version coverage, known limitations (WordStar 6's bare .pl, WordStar 3
status), and how behaviour gets verified: FAQ.md, with the
technical account in ERAS.md.
Install
$ brew install jonmichaels/tap/ctrl-kd # macOS / Linuxbrew
$ pipx install ctrl-kd # or: pip install ctrl-kd
Python ≥ 3.9, no dependencies. Library API: ctrlkd.convert(data, to='html').
Adding an output format
An output format is one function over the parsed document — register it with the
@ctrlkd.emitter decorator, or ship it as a pip-installable plugin via the
ctrlkd.emitters entry-point group and it appears in the CLI automatically.
EXTENDING.md has the IR contract, a complete worked example
(BBCode in ~40 lines), and a checklist.
Siblings
soft-return — CtrlKD, a Swift
port of this engine, verified byte-for-byte against this implementation via
machine-generated test vectors (the two projects found six real bugs in each
other during the port). It grows the sr CLI and the Soft Return macOS app.
Lineage
Standing on the shoulders of the tools and documentation that kept WordStar
readable: Yohanes Nugroho's WS-CON, Michael Petrie's English port, the wsconvert
project, Robert J. Sawyer's WordStar archive, and the WordStar format
documentation community. Behaviors were studied and reimplemented; no code was
copied. The development corpus is personal and is not distributed — tests use
synthetic fixtures that encode the same behaviors.
Credits
Written by Jon Michaels — whose 1987–1992 WordStar files, and the need to read them again, are the reason this exists — with Athena (Claude, Anthropic) as co-author: the byte archaeology, the wrap test, and the implementation grew out of a joint effort to recover those disks. Every commit carries the co-author trailer.
License
MIT © Jon Michaels
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ctrl_kd-3.0.0.tar.gz.
File metadata
- Download URL: ctrl_kd-3.0.0.tar.gz
- Upload date:
- Size: 119.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c57773597de1c9ef8a9e964cab9a09c59663e0f950185241c547cf253e9cd6da
|
|
| MD5 |
cfce2447fb4e8d7bc14c336c5e7f09e7
|
|
| BLAKE2b-256 |
53371b11ccdae14b30e6f7f8819e2b895d98d1dff8ec5042d140e39975e229a3
|
Provenance
The following attestation bundles were made for ctrl_kd-3.0.0.tar.gz:
Publisher:
publish.yml on jonmichaels/ctrl-kd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctrl_kd-3.0.0.tar.gz -
Subject digest:
c57773597de1c9ef8a9e964cab9a09c59663e0f950185241c547cf253e9cd6da - Sigstore transparency entry: 2341335483
- Sigstore integration time:
-
Permalink:
jonmichaels/ctrl-kd@e015f6ed2f2628756620ea46a88ddd5dc56aa536 -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/jonmichaels
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e015f6ed2f2628756620ea46a88ddd5dc56aa536 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ctrl_kd-3.0.0-py3-none-any.whl.
File metadata
- Download URL: ctrl_kd-3.0.0-py3-none-any.whl
- Upload date:
- Size: 82.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
594064da7df3f70b9afdea6f58637445227322901199ef8c145394c485ff527f
|
|
| MD5 |
0bae46284ef4f9f4b6ab5cb8dff92761
|
|
| BLAKE2b-256 |
5496751f3f012bc7e77ea5965c5ea8f980c9aa411cdaf360f00a92d7bf315eed
|
Provenance
The following attestation bundles were made for ctrl_kd-3.0.0-py3-none-any.whl:
Publisher:
publish.yml on jonmichaels/ctrl-kd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ctrl_kd-3.0.0-py3-none-any.whl -
Subject digest:
594064da7df3f70b9afdea6f58637445227322901199ef8c145394c485ff527f - Sigstore transparency entry: 2341335502
- Sigstore integration time:
-
Permalink:
jonmichaels/ctrl-kd@e015f6ed2f2628756620ea46a88ddd5dc56aa536 -
Branch / Tag:
refs/tags/v3.0.0 - Owner: https://github.com/jonmichaels
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e015f6ed2f2628756620ea46a88ddd5dc56aa536 -
Trigger Event:
push
-
Statement type: