3: Your First Document - transcript

Download the MP3

Liam: The QUILL Cast, episode three. I'm Liam. Jessica is here, and today we are opening, saving, and protecting a document for the first time, all the way down to the code that sits underneath the keys you press.

Jessica: A quick frame for anyone new to the series. This is a fifty-four episode audio course on QUILL, the free, screen-reader-first writing studio from Community Access. Built for Windows with macOS also supported. The whole design rule, in one sentence, is that QUILL is what a word processor looks like when blind users are the primary audience instead of an afterthought.

Liam: Last episode was install and first launch. The short version: you downloaded a small signed installer, ran it, and the wizard met you. You picked an intent profile, set your keyboard pack, set your data location, hit Finish, and landed in a fresh untitled document in a tab. If you skipped it, you can run the wizard any time from Help, Personalise QUILL. The whole thing is non-destructive and re-runnable.

Jessica: Today's frame is even smaller. Today is about one verb, in three of its tenses. Make a document, save a document, reopen a document, and the three layers of trust that make the round trip safe. We will walk the File menu end to end, the new and open document paths in the source, the recent files machinery, the per-document position memory, the autosave and crash recovery net, and the honest corrections where the docs drifted from the code. By the end you will have a real first document and you will not be afraid of it.


Liam: Before we get into keystrokes, a do this now beat, because today's episode only sticks if you actually open QUILL. If you have not done it yet, pause the audio, install from the QUILL GitHub releases, run the installer, finish the wizard, and land in the editor with an empty tab in front of you. If you have already installed, you are ready. When you come back we are going to make a tiny first document, save it, close QUILL, and reopen it.

Jessica: We will not insert a real silence marker for the pause. If you are hands-on, take a real break. If you are listening on a walk, keep walking, the order still makes sense, you can do the steps later.

Liam: Step one of today's walk, the verb new_file. The keymap default is Control N, bound in the default keymap to file dot new, which routes to core dot file. In quill slash ui slash main_frame, the function is named new_file, and the body is small on purpose. It calls _clear_empty_workspace_state, builds a brand new Document, creates a document tab for it, selects the tab, resets the per-document persistent undo history, resets the location ring, and posts the status New document. Then it fires a Quillin event called document dot created. No dialog, no file picker, no template picker. A blank canvas in a tab.

Jessica: And the keyboard shortcuts to know for later are Control N for new, Control O for open, Control S for save, Control Shift S for save as. Those four bindings live in quill slash core slash keymap dot py. The JAWS compatible, NVDA compatible, Narrator compatible, and the QUILL default packs all use the same four. The QUILL key, the one we will learn in a later episode, has its own chord versions of the same verbs. Today we are staying on the standard keys.

Liam: Step two, open. The keymap default is Control O, and the function is open_file. The body is more interesting than new_file because it has to handle a dozen real-world shapes. If you call it with no path, it pops a file picker, the native wx dialog or the simpler keyboard-friendly QUILL dialog, depending on a setting called use_simple_file_dialog. If you pass a path, the function walks a clear sequence.

Jessica: The first check is whether the file is already open. If there is already a tab for that path, the function just selects the existing tab, records the path as recent, positions the editor, and posts Opened, then the file name. The open is a no-op, which is the right answer. You do not want two tabs editing the same file at the same time.

Liam: The second check is the trust boundary. QUILL keeps a list of trusted folders, and if the file you are opening is not in one of them, the function prompts you with _prompt_untrusted_location. You can trust the folder once, trust it forever, or cancel. The default behavior is, you are asked, you decide.

Jessica: The third check is the suffix. The function lower-cases the suffix and routes by extension. PDF, DOC, DOCX, and the other office formats go to a worker thread, because the parser is heavy and the UI thread must never block. The function captures the Word open mode and the docx read engine setting on the UI thread first, then hands the read to the task manager, which delivers the result back through wx dot CallAfter to _finish_open_document. CSV and TSV take a slightly different path through _resolve_csv_open_mode, which can send the file to the Table Studio grid if that experimental gate is on. Everything else, plain text, Markdown, HTML, RTF, EPUB, runs synchronously on the UI thread because the reads are fast.

Liam: For today, remember three things. The trust prompt happens before the read. Heavy formats run on a worker. Tabs are deduped by path. That is the open contract.


Jessica: Now the first quality-of-life layer, Open Recent. On the File menu, the second item, right under Open, is a submenu called Open Recent. Under the submenu is a list of the last ten files you opened, with a separator, and a Clear Recent Files entry. The data lives in a single file in your QUILL data directory.

Liam: The file is recent dot json, in app_data_dir. The module that owns it is quill slash core slash recent dot py, and there are four functions to know. load_recent_files reads the JSON list and returns a list of Path objects. save_recent_files takes a list of Path objects, normalizes each to a string, and writes them atomically through write_json_atomic. add_recent_file resolves the path, dedupes against the existing list, prepends the new entry, and truncates to the limit. clear_recent_files saves an empty list.

Jessica: The limit comes from a setting called recent_files_limit, and the default is ten, with a hard clamp between one and fifty in the settings validator. So the menu shows up to ten entries, and if you want more or fewer, change the setting, and the next menu refresh respects it.

Liam: The second setting, recent_files_auto_clear_missing, is the more interesting one. The default is False. If you turn it on, on every startup QUILL walks the recent list and asks, for each entry, is this file still on disk, and if the answer is no, drop it from the list. Entries on removable drives, USB sticks, network shares, anything that is not a confirmed fixed internal drive, are never even probed. The function _is_fixed_drive uses the Windows GetDriveType API on Win32, and returns False on macOS.

Jessica: The reasoning is in a comment in prune_missing_recent_files. A file that is missing on a USB drive is almost always missing because the drive is unplugged, not because the file was deleted. A user with a hot-swappable drive, or a network share that is offline, would see their history wiped on every reboot. The conservative answer is to never probe a non-fixed drive. If you really want missing fixed-drive entries gone, turn the setting on.

Liam: Honest correction number one for this episode. The first draft of this audio course described Open Recent as if it always pruned. It does not. The default is no auto-prune. We are saying it out loud because the user guide and the code disagree, and the code is right.


Liam: Step three, save. The keymap default is Control S, and the function is save_file. The body is where most of the safety lives. The first guard is a read-only check. If the active tab is read-only, the function routes to save_copy_remote, which writes a copy elsewhere without touching the original. The second guard is the no-path check. If the document has never been saved, save_file calls save_file_as, which shows the Save As dialog. The third guard is the export-only check.

Liam: The export-only check is one of the more thoughtful design decisions in the product. There is a list called EXPORT_ONLY_SUFFIXES in quill slash io slash export dot py, containing pdf, doc, odt, epub, pages, ppt, pptx, xls, xlsx, sqlite, and db. If the file you opened has one of those suffixes, the buffer in front of you is extracted text. Writing that text back over the original would destroy the binary, so save_file refuses, shows a message box that explains the situation, and routes to save_file_as.

Liam: Once those guards pass, the real work happens. If the document is modified, the function calls backup_document, which writes a timestamped copy into the backups folder under your QUILL data directory. Then it calls _write_document_to_disk, which routes to the right io function for the suffix. If the write raises an OSError, the function shows an error dialog and posts Could not save. The document is left modified, so you can fix the underlying problem and try again.

Jessica: On a successful write, the function persists the document's bookmarks and last cursor position to DocumentMemory, primes the external-change watcher, records the persistent undo state, flushes it, refreshes the title bar, posts Saved, then the file name, to the status line, and fires a sound event, DOCUMENT_SAVED. The whole sequence is one synchronous block on the UI thread. For plain text and Markdown, the write is microseconds, so there is no debouncing.

Liam: The Save As dialog, Control Shift S, is a wx dot FileDialog in save mode with FD_OVERWRITE_PROMPT set, with a five-choice type filter. Index zero is text files, dot txt. Index one is Markdown, dot md. Index two is HTML, dot htm, dot xhtml. Index three is Rich Text Format, dot rtf. Index four is Word Document, dot docx. The default filter for an untitled document is governed by a setting called default_new_document_format, defaulting to markdown.

Jessica: A subtle point worth knowing. The filter you select only takes effect if you did not type an extension. If you type notes dot txt with the HTML filter highlighted, you get notes dot txt, not notes dot txt dot html. The typed extension always wins. The function _resolve_save_target handles this. If you typed nothing, the function falls back to the selected filter, and the default for the catch-all All files choice is Markdown.

Liam: Save All walks every tab in order, calls save_file on each, and bails out at the first save that comes back still modified. The status reads Saved all documents when everything went through, or Save all cancelled, and you can fix the offender and try again. The walk is in document tab order, which is the natural one.


Jessica: Step four, position memory. The second quality-of-life layer. Close a document at line four hundred, reopen tomorrow, and the cursor lands back at line four hundred. The mechanism is called DocumentMemory, it lives in quill slash core slash bookmarks dot py, and the on-disk file is document_memory.json in your QUILL data directory.

Liam: The shape of the file is a JSON object keyed by the document's normalized path. The key is produced by key_for, which does os dot path dot normcase of os dot path dot abspath. On Windows that is case-insensitive, so Notes dot md and notes dot MD land in the same slot. The value is a small object with a bookmarks dict and a last_position integer. The loader is forgiving, missing or malformed files yield empty in-memory values, and the editor still starts. The save function is strict, normalizing names by stripping whitespace, clamping positions to zero or above, and writing the file atomically.

Jessica: When does the position get written. The trigger is _remember_active_caret, which is called from save_file, from reload_from_disk, from Save As after the rename, and from the editor close path. The function reads the editor's current insertion point, computes the document key, and calls set_last_position. The restore path is _restore_document_memory, which fires when a tab is built for a saved document, pulls the saved position, clamps it to the new editor's last-position, and calls SetInsertionPoint. It also calls ShowPosition, which scrolls the visible region. The function also restores the document's named bookmarks from the same entry, so any bookmarks you set on a document survive the round trip too. Bookmarks are a separate, larger topic, episode nine, but the storage and the save are part of position memory, and the same key is used.

Liam: Honest correction number two. The first draft of this episode said bookmarks are restored when you reopen a document. That is true, but only if you actually saved the document. An untitled document that you saved as something will remember its bookmarks under the saved path. An untitled document that was never saved has no path, key_for returns None, and nothing is persisted. The bookmark persists for the session, in memory, and goes away when QUILL closes. That is the right behavior. The function set_bookmarks and set_last_position both early-return on a None key, with no error and no warning. We are calling it out so you do not get confused.


Liam: Step five, the safety net. The third and most important layer. As you work, QUILL silently writes recovery state. If QUILL closes unexpectedly, crash, power cut, accidental shutdown, the next launch notices and offers to restore what you were doing. The mechanism has three parts, the autosave snapshots, the session lifecycle, and the crash recovery dialog.

Jessica: Let's start with autosave. The function in main_frame is _maybe_autosave, called from a handful of editor mutation points. It checks the configured interval, default thirty seconds, and skips if not enough time has passed. It also skips if the document's text has not changed since the last snapshot. If the conditions are met, it calls autosave_document and then calls save_cursor_position from the recovery module with the editor's current insertion point.

Liam: The autosave module is quill slash core slash autosave dot py. autosave_document takes the document, the session id, and an optional max_snapshots, defaulting to ten. It builds the per-session folder under app_data_dir slash autosave slash session_id, and inside that folder it writes a file whose name is a sha1 of the document's resolved path, plus a UTC timestamp, plus a three-digit counter that breaks ties when two autosaves land in the same microsecond. Without the counter, two snapshots could sort in the wrong order and the recovery layer would resurrect the older one.

Jessica: After the write, the function globs the folder for the same document key, sorts by name, and deletes everything past the cap. The default cap is ten snapshots, the last five minutes of work at thirty-second intervals. Write order, not clock collision.

Liam: The session lifecycle lives in quill slash core slash recovery dot py. On startup, main_frame generates a fresh uuid4, stores it as self dot session_id, and calls begin_session. Begin_session takes the session id, validates it as a UUID string, acquires a process-wide RLock and an OS-level file lock on recovery_state dot json dot lock, reads the current state, and decides whether to return a recovery offer.

Jessica: The state file is also in app_data_dir, and it is called recovery_state dot json. The shape is a JSON object with last_session_id, clean_exit, cursor_positions, recovery_dismissal_counts, and last_recovery_offer. The state is read and written through read_json and write_json_atomic. The RLock serializes in-process callers. The OS file lock, on Windows via msvcrt dot locking, and on POSIX via fcntl dot flock, serializes two processes that might race for the same data directory.

Jessica: The decision to offer recovery is simple. If the previous session id is a non-empty string and clean_exit was False, the function looks up the latest non-empty snapshot for that session in the autosave folder, checks whether the offer was already dismissed, and if not, builds a RecoveryOffer with the session id, the snapshot path, the saved cursor position, and the dismissal count. If you exit cleanly, mark_clean_exit flips clean_exit back to True. If the session id in the state does not match, mark_clean_exit is a no-op, which prevents a late shutdown from one window from poisoning another.

Liam: The crash recovery dialog is _offer_crash_recovery, which runs from main_frame's startup after the wizard. If there are no offers, it returns immediately. If there is one offer, it prepares a small payload on a background thread, because reading the snapshot can be slow on a large autosave file. The worker reads the snapshot, splits it into lines, takes the first thirty, and returns a preview string. The main thread builds a modal dialog titled Crash Recovery, sized 780 by 520, with a snapshot preview, a read-only logs-folder field, and a row of six buttons.

Jessica: The buttons, in order, are Restore Latest Snapshot, Open Logs Folder, Clear Logs, Save Diagnostics dot dot dot, Send Bug Report, and Skip Recovery. The default button is Restore, focus is set to it, and the modal-id contract is applied so Enter triggers the affirmative and Escape triggers the skip. If you have dismissed the offer three or more times, the skip button label changes to Discard and Continue, and the introductory text becomes a more direct ask.

Liam: The Restore path reads the snapshot again on the UI thread, creates a new untitled Document with the recovered text, marks it modified, fires the Quillin document dot opened event, marks the offer recovered, resets the location ring, and then, if the saved cursor position is greater than zero, schedules a wx dot CallAfter to set the editor's insertion point to that position. The status reads Recovered latest autosave snapshot. If the snapshot had bytes that could not be decoded as UTF-8, the function adds a parenthetical to the status, some bytes replaced, check notifications, so you know to look.

Liam: The Skip path marks the offer dismissed, increments the dismissal count, and continues with the blank workspace. If the previous session still shows clean_exit False because you never marked a clean exit, the offer will be re-surfaced. That is the design. The recovery is offered, not forced.

Jessica: A few small but important clarifications, because today's episode is the foundation for the rest of the series, and the small print matters.

Jessica: First, on the keys. Control N, Control O, Control S, Control Shift S are the defaults, and they are the same in the JAWS compatible, NVDA compatible, Narrator compatible, and the QUILL default packs. The only divergent binding is in the JAWS legacy rebindings block in keymap dot py, which restores the old JAWS-style Save As on F12. If you load the JAWS pack and find Save As on F12, that is the legacy rebinding, and you can flip it back from the keyboard remapper.

Liam: Second, on tabs. The verb next_document is Control Tab in the default keymap, and previous_document is Control Shift Tab. The behavior wraps around, so the verb feels like a ring, not a line. The tab label is announced as you land, the editor focus follows the new tab, and the Window menu lists every open document for direct jumps by name.

Jessica: Third, on the New Document from Clipboard item, which the previous draft of this episode called New from Clipboard. The actual menu label is New Document from Cli&pboard, with the ampersand on the p, and the keymap command is power dot new_document_from_clipboard. It is a power-tools item, not a stock wx id, and it lives in the File menu between the New group and the Save group. The function reads the clipboard, builds a new Document with the text, and opens it in a tab.

Liam: Fourth, on what happens if the document is on a network share or a USB drive. The trust prompt fires on the first open from that location, and you can trust the folder once or forever. Autosave still works, because the autosave folder is in your QUILL data directory, not next to the document. If the network share goes down, your work is safe in the local autosave folder, and the recovery dialog will surface it on the next launch. The autosave is always local, never next to the open file.


Jessica: Now an honest correction beat, because we said at the top of the series that we would verify every claim against the code, and that means calling out where the docs and the code have drifted apart.

Jessica: Correction one. The first version of this episode said Open Recent auto-prunes missing files. It does not, not by default. The setting recent_files_auto_clear_missing defaults to False, and the prune function is conservative on top of that, never probing removable or network drives. That is the design, and the design is right.

Jessica: Correction two. The first version of this episode said New from Clipboard was a stock File menu item. It is a power-tools item, with the full name New Document from Clipboard, and the keymap command is power dot new_document_from_clipboard. If you cannot find it on the File menu, your profile is filtering it out, and the command palette will reach it from any profile.

Liam: Correction three. The user guide shipped with zero point nine describes the crash recovery dialog as having a single Restore button and a Skip button. The shipped code has six buttons, with the four in the middle being Open Logs Folder, Clear Logs, Save Diagnostics, and Send Bug Report. The user guide is the one that is out of step.

Jessica: Correction four. The user guide also implies the recovery dialog only shows up if you had an untitled document open. It shows up if any document was modified since the last clean exit, including saved documents. The current dialog restores one document at a time, the most recent snapshot, and the other autosaves are preserved in their per-session folders for you to find by hand. A multi-document recovery picker is a feature request, not a shipped feature.

Liam: Let's put it all together. The lifecycle of a first document in QUILL looks like this.

Jessica: You press Control N, you get a fresh untitled document in a tab. You type. Every thirty seconds, if the text has changed, an autosave snapshot lands in the per-session folder, and your cursor position is written to recovery_state dot json. You press Control S, the document is named if it has a name, or routed to Save As if it does not. A backup is written to your backups folder, the document is written to the chosen path, the title bar's [modified] marker goes away.

Liam: You close the document with Control W, or you close QUILL with Alt F4. The shutdown sequence calls mark_clean_exit, which flips clean_exit to True. The next launch, begin_session sees clean_exit True, returns no offers, and the editor starts normally. If the editor crashes, the clean_exit flag is never set, the next launch sees clean_exit False, builds a recovery offer, and shows the dialog. You Restore, you get your document back, in a new untitled tab, marked modified, with your cursor where you left it.

Jessica: You open the document a week later from Open Recent. The menu finds it in recent dot json, the file opens in a tab, _restore_document_memory sets the cursor, the bookmarks reappear, and you are back exactly where you were. That is the design.


Liam: Homework, four steps, about five minutes. Step one, make a new document with Control N, type a paragraph, save it with Control S. If it is untitled, the Save As dialog asks where to put it. Pick a folder you can find again, give it a name, Finish. Note the status line reading Saved and the file name. Step two, close the document, close QUILL entirely, reopen QUILL. The wizard does not run, because you are not on a fresh install. Open the File menu, find Open Recent, and confirm your new file is in the list. Click it, the tab opens, and the cursor lands at the end of the document, which is where the autosave cursor position was when you saved.

Jessica: Step three, copy some text from anywhere, a web page, a chat, another document. Back in QUILL, run New Document from Clipboard. The copied text opens in a new tab, ready to edit. Step four, the most important one. Force a clean test of the recovery net. Create a new document, type several paragraphs, do not save, and from the operating system, kill the QUILL process. Relaunch QUILL. The crash recovery dialog should appear with a snapshot preview of what you typed. Restore, and the document is back, with the cursor near where you were working.

Liam: Step four is the one that turns today's claims into today's knowledge. Do not be afraid of the dialog. The net is real, and you will probably only have to use it once in a year of writing.


Jessica: Next episode is the main window. The menu bar top to bottom, the tabs and the cycling verb, the status bar as a narrator instead of a decoration, the spoken echo that holds the last twenty announcements, the modal-dialog contract enforced by an automated gate, and the focus law that says nothing steals focus. Episode four turns the geography of the application into a mental map, and episode five hands you the command palette.

Liam: The series total is fifty-four episodes. We are at three. The next fifty-one cover editing, formats, the safety stack in depth, search and replace, compare and differences, spell, thesaurus, languages, markdown structure, the everyday writing style, the speech and reading arc, the AI arc, the accessible vault, the story studio, the braille production pipeline, the Quillins, the audio studio, the publishing arc, the build-your-own-Quillin arc, and the trust and community finale. The everyday-editor arc ends around episode eighteen. The AI arc opens at nineteen.

Jessica: A small reminder of the house style. The user guide says Open Recent always prunes missing files. The code does not. The user guide says the recovery dialog has two buttons. The code has six. The user guide implies recovery is for untitled documents only. The code recovers any modified document. We will keep saying so until the docs catch up.

Liam: One last thing. The episode you are listening to right now was written by people who used the same File menu, the same Save As dialog, and the same crash recovery dialog you are about to use. The system works. The system is honest about its limits. The system is the most boring part of a writing studio, on purpose, because boring is kind.

Jessica: I'm Jessica.

Liam: And I'm Liam. Save early, save often, but honestly, QUILL has got you either way.

Back to all episodes