diff --git a/.editorconfig b/.editorconfig index 618a38f..9ffd972 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,6 +16,10 @@ end_of_line = crlf [*.md] trim_trailing_whitespace = false +# Inno Setup reads a script or a message file without a BOM in the ANSI code page. +[*.{iss,isl}] +charset = utf-8-bom + [*.{json,yml,yaml}] indent_size = 2 @@ -48,6 +52,13 @@ csharp_space_before_colon_in_inheritance_clause = false csharp_space_after_colon_in_inheritance_clause = true csharp_preserve_single_line_statements = false +# Keywords (string, char) for declarations, framework types for static members +# (String.IsNullOrEmpty, String.Empty, Char.IsWhiteSpace), as the template writes them; +# dotnet format enforces it (IDE0049). +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = false +dotnet_diagnostic.IDE0049.severity = warning + # .NET code analysis dotnet_analyzer_diagnostic.category-security.severity = error dotnet_analyzer_diagnostic.category-performance.severity = warning diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e2a88c1..e34a320 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -48,7 +48,7 @@ jobs: # Localization.GetCurrentCulture silently falls back to en-US for every language. - name: Compile translations shell: pwsh - run: ./src/WinFormsTemplate/locale/scripts/Compile-Translations.ps1 -Strict + run: ./src/PlanCake/locale/scripts/Compile-Translations.ps1 -Strict - name: Restore dependencies run: dotnet restore diff --git a/.gitignore b/.gitignore index 1b4e988..dcdbe67 100644 --- a/.gitignore +++ b/.gitignore @@ -426,3 +426,12 @@ FodyWeavers.xsd # msgmerge backups *.bak + +# NetSparkle signing keys: the private key must never be committed (see "Updates" in CLAUDE.md). +keys/ + +# Installer output (built installers, portable zip and appcast) +installer/Output/ + +# Deploy configuration (contains the SSH host and path) +installer/deploy.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 651d28f..89eadc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,43 +1,56 @@ # Changelog -All notable changes to this project are documented here. The format follows +All notable changes to PlanCake are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [semantic versioning](https://semver.org/spec/v2.0.0.html) driven by GitVersion: a release is -a `vX.Y.Z` tag, and nothing carries a version literal. - -## [Unreleased] - -### Added - -- Test project (xUnit, AwesomeAssertions, coverlet) covering the configuration round trip and - the localization fallback, running on every push. -- GitHub Actions workflow: gettext install, translation compile, `dotnet format` check, build - and test. -- Dependabot for NuGet and Actions, plus release-note categories. -- GitVersion for all version numbers. -- `locale/` scaffolding: the gettext scripts, a POT template and the translation workflow. -- `.gitattributes` normalizing line endings to LF, except in `.bat` and `.cmd`. -- `ExitCode` constants and unhandled-exception logging for background threads and dropped - tasks. -- Portable mode: a `userdata/` folder next to the EXE moves all user data out of `%APPDATA%`. - -### Changed - -- Target framework raised to `net10.0-windows`; platform pinned to x64. -- `Directory.Build.props` folded into the project file. -- Solution converted to the XML `.slnx` format and moved to the repository root. -- Packages updated to current versions and aligned with the ones shipping applications use. -- `Config.Load` and `Config.Save` no longer show dialogs or call `Application.Exit` from a - static utility class: a missing file is written with the defaults and an unreadable one is - logged and falls back to them, so a bad INI cannot stop the app from starting. -- `Localization` gained `SetLanguage`, a `LanguageChanged` event and a thread-safe catalog, - so the language can be switched at run time. -- `Utils/Constants/Application.cs` renamed to `App.cs`; the old name shadowed - `System.Windows.Forms.Application` in every file that imported it. -- Forms moved under `Ui/`, mirroring the namespace layout of shipping applications. -- Copyright year updated to 2026. - -### Removed - -- The hardcoded `1.0.0.0` version. -- `BaseOutputPath` / `BaseIntermediateOutputPath` overrides pointing at `$(SolutionDir)`. +a `vX.Y.Z` tag, and nothing carries a version literal. The release notes shown in the update +window come from `changelogs/.md`. + +## [1.0.0] — unreleased + +### Initial Release + +PlanCake lets you read Markdown files comfortably and leave notes right where they belong. It +was made for the long implementation plans that AI coding assistants write, and works with any +Markdown file. It runs as a window, or without one from the command line. + +### Features + +- **Read Markdown as a document**: real headings, lists, tables and code blocks instead of hash + signs and asterisks, with GitHub-style tables, task lists, footnotes and more. Zoom from 50 to + 300 percent. +- **Leave notes on any block**: click a paragraph, heading, list item, table row or code block, + or press Enter on it, and type a note. PlanCake writes it straight into the `.md` file, right + after that block, between a pair of markers (`[usernote]` … `[/usernote]` by default). There + is nothing to save and no side file. +- **Notes in the document and in a list**: notes show after their blocks, with the Markdown in + them formatted, and in a notes list beside the document. Edit, delete, delete all, jump from + note to note with F9 and Shift+F9, and undo and redo everything PlanCake writes. +- **Check off tasks**: task-list checkboxes toggle from the document and write `[x]` or `[ ]` to + the file, after a question that can be turned off. +- **Open files every way**: from the command line, the Open dialog, drag and drop, the clipboard + (a copied file or path), and a link, which is downloaded to the Downloads folder (GitHub file + links work as they are). Links between Markdown files open in the same window, with Back and + Forward. +- **One window per file**: opening a file that is already open brings its window to the front, + so two windows never write into the same file. +- **Follows changes on disk**: when something else changes the file, PlanCake reloads it and + keeps your reading position, or asks first if you prefer. A note is never written over a + change you have not seen. +- **Safe with older files**: a file in a legacy encoding opens read-only unless you let PlanCake + convert it to UTF-8, and a file whose encoding cannot be recognized is never changed. +- **Command line** for scripts and AI assistants: `plancake list` (text or JSON), `check` (exit + code 3 while notes remain), `clear` and `export` to a standalone HTML page, with other markers + for a single run. +- **Keyboard, mouse and screen readers alike**: every action works from the keyboard alone and + with the mouse, block-to-block movement with Alt+Shift+Down and Up Arrow, and status messages + that screen readers speak wherever the focus is. Keyboard shortcuts are listed in the Help + menu. +- **Settings** for the interface and document languages, the note markers, what Enter and a + click do, confirmations, reloading and update checks, applied without a restart. +- **Six languages**: English, Russian, Ukrainian, French, Hebrew (with a right-to-left + interface) and German, for the interface, the user manual (F1) and the installer. The + document language is set apart from the interface language. +- **Automatic updates**, signed, with adjustable frequency. +- **Installer, portable zip and winget** (`Oire.PlanCake`). The installer puts `plancake` on the + `PATH` and installs the .NET 10 Desktop Runtime and the WebView2 Runtime when missing. diff --git a/CLAUDE.md b/CLAUDE.md index d6a3e30..a329c86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,11 @@ -# winforms-template — Repo Conventions +# PlanCake — Repo Conventions -Template repository for Oire Software's Windows Forms applications. .NET 10, Windows x64 only. -Everything here is meant to be copied into a new product repo and renamed; see the README for -the rename checklist. +PlanCake reads Markdown files (above all long implementation plans) as rendered HTML in +WebView2 and writes the user's notes on them straight into the `.md` file. It runs as a window, +or headless with a subcommand (`list`, `check`, `clear`, `export`). .NET 10, Windows x64 only. -When adapting this template for a new application, apply every convention below to the new -repository as well — that is the point of the file. +The repository was created from Oire's `winforms-template`; the conventions below come from it +and stay binding. The implementation plan is `docs/plans/completed/001-plan-cake-v1.md`. ## Accessibility is load-bearing @@ -16,9 +16,19 @@ decisions. `FlowLayoutPanel`. - **Labels:** use real `Label` controls with their `Text` property. **Do not** rely on `AccessibleDescription` / `AccessibleName` as a substitute — screen readers associate a - label with a control best through a visible `Label` next to it. + label with a control best through a visible `Label` next to it. The one exception is + `NativeListView`: screen readers do not read a preceding label for a list view, so it is + named through its own `AccessibleName` (the library's documented way; set it through a + `NativeListView`-typed reference, and again after anything that walks `Controls`). Keep the + visible label as well. The system proxy of a list view ignores the window text that + property sets, so PlanCake also names the list window through MSAA annotation + (`Utils/WindowAccessibleName.cs`); without it JAWS announces no name. - **Keyboard alternatives:** every mouse interaction (drag, hover, context menu) must have a keyboard equivalent. No exceptions. +- **Confirmations close with Escape, meaning No.** A Yes/No `MessageBox` has no cancel, so + Escape and the close button do nothing there: ask every yes-or-no question through + `DialogHelper.Confirm` (a task dialog with Yes, No and `AllowCancel`), never + `MessageBoxButtons.YesNo`. - **Native controls:** prefer `Oire.WinForms.NativeControls` over the stock WinForms owner-drawn equivalents where it offers one — the native peers are what screen readers actually understand. @@ -26,13 +36,13 @@ decisions. ## Localization — GetText.NET, not .resx -`.po` / `.mo` catalogs live in `src/WinFormsTemplate/locale/`. **`.resx` files are forbidden** +`.po` / `.mo` catalogs live in `src/PlanCake/locale/`. **`.resx` files are forbidden** — if the WinForms designer generates one, delete it. - **`Localizer.Localize(this, Localization.Catalog)`** in a form's constructor — walks the control tree and translates designer-set text. - **`_("literal")`** for strings built at run time. Import via - `using static Oire.WinFormsTemplate.Utils.Localization;`. Plurals `_n`, context `_p`, + `using static Oire.PlanCake.Utils.Localization;`. Plurals `_n`, context `_p`, both `_pn`. - **`.mo` files are build output and are gitignored.** CI compiles them from the `.po` sources with `Compile-Translations.ps1 -Strict` before `dotnet build`. Skip that and the @@ -41,10 +51,34 @@ decisions. entry out of the `.mo` and the app then shows English while looking translated. - **Menu mnemonics must be unique per menu level in every catalog.** A duplicate `&` letter within one menu throws at startup, on that locale only. -- Adding a language means updating `` *and* the language picker in - the settings UI. Both are easy to forget. +- **Two languages, never derived from each other.** The *interface language* (menus, dialogs, + the page's chrome and announcements) and the *document language* (the `lang` of the rendered + plan, which picks the screen reader's voice, and a hint for legacy encodings) are separate + settings with separate View menus. An English plan is read with the English voice whatever + the interface language is. +- **Adding a language** touches more than the catalog, and each piece is easy to forget: + `locale//PlanCake.po` (the interface list is built from the `locale\\` folders + holding a `.mo`, so the settings picker and View → Interface language follow by themselves); + `` in the `.csproj`; `LanguageList.SupportedCodes` (the document + languages and the menu order) and `LegacyEncoding.CodePageOf` (its legacy code page); the + manual, `help//manual.html`, with a glossary in `help/glossaries/`; and the installer, + `installer/Languages/Custom..isl` plus a `[Languages]` line in `plancake.iss`. -See `src/WinFormsTemplate/locale/README.md` for the script workflow. +- **A new dialog:** set its title in the constructor right after `Localizer.Localize` + (`Text = _("…");`), because the extractor misses a designer `Text =` on the form itself; call + `TextDirection.Apply(this)`; send every message box through `DialogHelper`; and add the form + to the lists in `MnemonicTests` and `TextDirectionTests` (both fail until you do: each checks + that its list covers every `Form` type). +- **The user manual follows the UI.** `help//manual.html` (six languages) is written by + hand with the `write-manual` skill, using the glossaries in `help/glossaries/`. It repeats + menu names, shortcuts and setting labels word for word, and no test checks it. A change to a + command, a key, a menu item or a setting updates all six manuals in the same change. + +- The catalog is `PlanCake.po` / `.mo`, named after `App.Name`; the executable is `plancake.exe` + (`AssemblyName`). The gettext scripts read `App.Name` from `Utils/Constants/App.cs`, not the + `AssemblyName`. + +See `src/PlanCake/locale/README.md` for the script workflow. ## Versioning @@ -70,12 +104,22 @@ that tag. CI checks out with `fetch-depth: 0` because a shallow clone has no tag Single project with folder/namespace separation: ``` -src/WinFormsTemplate/ -├── Ui/ -- MainWindow and dialogs -├── Utils/ -- Config, Localization -│ └── Constants/ -- App, Logging, ExitCode +src/PlanCake/ -- namespace Oire.PlanCake, builds plancake.exe +├── Notes/ -- note parsing, reading and writing files, task toggles, notes JSON +├── Rendering/ -- Markdig to HTML with source line ranges, notes placed after their blocks +├── Ui/ -- MainWindow, DocumentView (WebView2), the host command table, dialogs +├── Cli/ -- the headless subcommands and the console they write to +├── Services/ -- update checks (NetSparkle), downloading a Markdown file from a link +├── Utils/ -- Config, Localization, single instance, file watching, status announcer +│ ├── Constants/ -- App, Logging, ExitCode +│ └── Enums/ -- setting values +├── web/ -- the page WebView2 shows: index.html, app.js, morph.js, blocks.js, app.css +├── help/ -- the user manual, help//manual.html, and its translation glossaries └── locale/ -- .po catalogs and gettext scripts -tests/WinFormsTemplate.Tests/ +tests/PlanCake.Tests/ -- namespace Oire.PlanCake.Tests +installer/ -- Inno Setup script, Build-Installer.ps1 +changelogs/ -- release notes per version, for the appcast +PlanCake.slnx ``` Do not split into more projects for organization's sake. Something genuinely shared across @@ -85,28 +129,258 @@ applications belongs in a NuGet package instead. and `UseWindowsForms`, an `Application` class in that namespace shadows `System.Windows.Forms.Application` in every file that imports it. +## Architecture + +The plan (`docs/plans/completed/001-plan-cake-v1.md`, Technical details) has the full rules; this is the +map. + +- **The file is the only store.** No Save command, no side file: a note is in the `.md` file the + moment the user confirms it, and the notes JSON (`plancake list --json`, File → Export notes) + is a copy PlanCake never reads back. +- **Notes/** is UI-free and shared by the window and the command line. `NoteParser` finds the + notes by the configured markers (`NoteMarkers`: a pair, or a single token that runs to the end + of its line) and strips them out, keeping a map from stripped lines to original ones. + `NoteStore` adds, edits and deletes notes and toggles tasks (`TaskToggle`), with undo and + redo; every operation takes the text the caller last rendered and throws + `StaleFileException` without writing when the file on disk differs, so a note never lands on + a change the user has not seen. `MarkdownFile` reads and decodes (File safety below) and + writes through a temporary file and `File.Replace`. +- **Rendering/** renders the stripped source with Markdig (`UseAdvancedExtensions`). Every block + the user can land on (paragraph, heading, a list item's leading paragraph, code block, table + row) carries `data-lines="start-end"` in original lines, and each note is a `role="note"` + element after its block. Line numbers are **1-based** wherever a user, a CLI consumer or + `data-lines` sees them; Markdig's 0-based lines are converted at the boundary. The strings the + renderer writes come in through `RenderStrings`, so it never touches the catalog. + `PositionRestorer` picks the block to return to after a re-render. +- **Ui/** holds `MainWindow` (the menu, the notes list, every note action), `DocumentView` (the + WebView2 control and its lockdown), `HostCommands` (the one table of commands and keys that + the menu, both key paths and the Keyboard shortcuts dialog all read) and `PageMessages` (the + page protocol's message shapes). +- **Cli/** is `CliRunner` (System.CommandLine 2.x: `list`, `check`, `clear`, `export`) and + `ConsoleAttacher`. `list`, `check` and `export` never write the Markdown file; `clear` follows + the conversion setting like the window. `list`'s text lines are data and stay in English; + other messages follow the interface language. `ExitCode.NotesRemain` (3) is `check` finding + notes, distinct from `ExitCode.Error` (1). +- **Services/** is `UpdateService` (Updates below) and `MarkdownDownloader` (File → Open from + link: http(s) only, a GitHub file page turned into its raw file, 30 seconds, 10 MB, into the + Downloads folder, always under a `.md` name and with a `Zone.Identifier` stream, as a + browser marks a download). +- **Links in a plan never run anything.** A plan may come from a download or an AI assistant, + and its link text can say anything. `LinkResolver` sorts a program or script + (`LinkKind.Program`: `.exe`, `.bat`, `.js`, `.lnk`, … and `PATHEXT` and `AssocIsDangerous`) + away from the shell (the window offers to show it in File Explorer), and does not follow, or + even check, a UNC path on another host than the document's, since touching it sends the + user's credentials there. File → Open in editor uses the "edit" verb or Notepad for anything + that is not Markdown, never the default verb. +- **web/** is the page. `app.js` shows what the host renders, reports which block or note the + user acts on, and moves focus when the host asks. `morph.js` updates the page in place when + the same file is rendered again; `blocks.js` picks the block Alt+Shift+Down and Up move to. + Both expose a pure function that the tests run in Jint (`MorphPlanTests`, `BlockPickTests`). + **No user-visible string is written in the page**: all of them come from the host in the + `strings` message, already translated. Focus moves through `focusElement` in `app.js`, which + adds a temporary `tabindex="-1"` and marks the element with `data-plancake-current`; + `app.css` draws the focus outline from that mark, not from `:focus`, which Chromium did not + show when the page moved the focus itself. An attribute the page manages at run time + (`data-lines`, `data-note`, `tabindex`, `data-plancake-current`) must be in `morph.js`'s + `volatileAttributes`, or every re-render counts it as a content change. The page's content + security policy (`index.html`) is the only thing keeping a plan's raw HTML from running + script; `PageSecurityPolicyTests` pins it. + +### Page protocol + +JSON messages through `chrome.webview.postMessage` / `PostWebMessageAsJson`, each with a `type`; +the full list is in the plan (Technical details → Page protocol) and the shapes in +`Ui/PageMessages.cs`. Host → page: `render` (HTML, document language, title, where to put the +focus), `strings`, `focusNote`, `nextNote` / `previousNote`, `nextBlock` / +`previousBlock`, `taskState`. Page → host: `activate`, `activateNote`, `contextMenu`, +`toggleTask`, `position`, `openLink`, `noMoreNotes`, `noMoreBlocks`, `dropFiles`, `goBack`, +`ready`. Every render carries a `generation`; the host ignores a message from an older render +(`WhileCurrent` in `MainWindow`). There is no `announce` message: every announcement goes +through `StatusAnnouncer`. + +A different file gets a freshly loaded page (the host navigates to `index.html` again and +renders once the page says `ready`). A re-render of the same file (an outside change, a reload, +a note action, undo) is patched in place by `morph.js`. Only a render the user asked for by +acting on a block carries focus; the rest leave the reader where they are. + +### WebView2 + +- **User data folder** under `App.DataFolder\WebView2`: the install folder is not writable. +- **The page's own host.** `web\` is mapped to `https://app.plancake/`. The only navigation + allowed is the host's own `Navigate` to a page there; every other navigation, frame navigation + and new window is refused, and the CSP in `index.html` allows no script but the app's own. + Links in a plan go to the host (`openLink`, `LinkResolver`), so raw HTML in a plan can neither + take the view away nor run script. Browser context menus, accelerator keys and the status bar + are off; dev tools are on in Debug only. +- **Accelerator keys.** Keys pressed while the page has focus never pass through the host's + message loop, so the native menu bar's accelerators never see them. The WinForms control does + not call `ProcessCmdKey` either: it raises its own `KeyDown` from the browser's + `AcceleratorKeyPressed`, which `DocumentView.AcceleratorKeyDown` passes on. `MainWindow` + sends that path and `ProcessCmdKey` through `HostCommands`; a new shortcut goes into that + table, never into a key handler. +- **`BeginInvoke` before any dialog, message box or menu** started from a WebView2 event + (`WebMessageReceived`, `NavigationStarting`, …) or from a key WebView2 forwarded. A nested + message loop inside those handlers re-enters WebView2. +- **Two different things named WebView2.** `WebView2Loader.dll` is a small native DLL from the + NuGet package: the single-file publish leaves it next to the exe, and the installer and the + portable zip ship it. The WebView2 *Runtime* (the Edge engine) is never presumed present: the + installer installs it when missing, and `Program` checks for it at startup and offers the + download page (`ExitCode.Error` without it). + +### JAWS findings that shape the code + +`docs/jaws-spike.md` records what JAWS does inside WebView2 and why the code is the way it is. +In short: + +- **No permanent `tabindex` on blocks or notes.** Enter on a focusable element puts JAWS in forms + mode. The page adds `tabindex="-1"` only while it moves focus to an element and removes it on + blur; a `keydown` Enter on a block or note is treated like a click, as a second line of defense. +- **Enter cannot be told from a click** (JAWS answers Enter with a mouse click), so both activate: + a block gets a note, a note is edited. A double-click does nothing more; a click that ends a + text selection does nothing. +- **Announcements are UI Automation notifications** (`StatusAnnouncer`), heard wherever the focus + is, the virtual buffer included. +- **Screen readers own keys.** JAWS takes F8, so notes move with F9 / Shift+F9; screen readers + take Alt+Down and Alt+Up, so blocks move with Alt+Shift+Down and Up. New key combinations keep + colliding: a command that does not need a shortcut gets a menu item without one (View → + Wider / Narrower notes list). +- **JAWS keeps its virtual cursor on DOM nodes.** Replacing the page's content throws it to the + top, hence `morph.js`; and a different file must be a fresh page load, or JAWS keeps its old + offset. +- **Notes are `role="note"` elements** with the role descriptions "user note" and, for braille, + "unote" (localized). A button style was tried and dropped: long labels are hard to listen to. + +### One window per file + +Every window is its own process. `Utils/SingleInstance.cs`: the window showing a file owns a +named pipe named after the SHA-256 of its normalized full path. Opening a file that is already +open, from the command line, a link, the history, the clipboard or anywhere else, connects to +that pipe, asks the owner to come to the front, and opens nothing (`Program` checks before +creating a window, `MainWindow` before showing a file). Two windows can therefore never write +conflicting notes into one file. The same process-per-window design is why update checks run in +one window only (Updates below). + +### Command-line output + +`plancake.exe` is a GUI-subsystem exe and gets no console of its own. `ConsoleAttacher` (only +for subcommands, `--help`, `--version` and parse errors; the window never touches the console) +writes redirected output straight to the redirection as UTF-8 without BOM, and calls +`AttachConsole(ATTACH_PARENT_PROCESS)` **only when standard output is not redirected**, so a pipe +or Claude's captured output is never taken over. The caveat to keep in every doc: interactive +PowerShell and cmd do not wait for a GUI exe, so attached output can appear after the prompt +returns, and in PowerShell `$x = plancake list plan.md` does not wait and leaves +`$LASTEXITCODE` unset. Piped or captured output (`| Out-String`, `| Out-Host`, `cmd /c`, bash, +Claude's tools) always waits and is complete. `CliRunner` takes its writers as arguments, so the +tests need no console. + ## Data locations -`App.DataFolder` resolves to `%APPDATA%\Oire\`, or to `userdata/` next to the EXE -when that folder exists (portable mode, detected once at static init). Config files sit at the -root of the data folder; user content goes under the `data/` subfolder, so clearing user data -never takes the settings with it. +`App.DataFolder` resolves to `%APPDATA%\Oire\PlanCake`, or to `userdata\` next to the EXE when +that folder exists (portable mode, detected once at static init). It holds `PlanCake.cfg` +(written to a temporary file and moved over the old one, since other windows read it), +`logs\` (Serilog: `PlanCake.log`, `PlanCake-short.log`, `errors.log`, `errors-short.log`, +`analysis.json`; every window and every CLI run is its own process, so while one holds a log, +the others write to numbered siblings such as `PlanCake_001.log`) and `WebView2\` (the +browser's user data folder, `App.WebView2DataFolder`). PlanCake keeps no other user content, so +`App.DataSubfolder` (`data\`) is unused. The logs are the first thing to read when a user +reports a problem. + +## File safety + +PlanCake never writes a file whose text came from a lossy decode (replacement characters, +U+FFFD, introduced by decoding): it would destroy what they stand for. A file that is not UTF-8 +is decoded only with a legacy encoding that decodes it cleanly (`Notes/LegacyEncoding.cs`: the +charset detector, then the document language's code page, then the ANSI code page unless that +is UTF-8, 65001); when none does, the file stays read-only and is never converted, whatever +the settings say. Two cases never reach the legacy code pages at all and stay read-only too: a +file whose BOM states its encoding but that does not decode in it, and UTF-8 with a few damaged +bytes (`LegacyEncoding.FindDamagedUtf8`), which a single-byte code page would turn into +mojibake. Tests inject the ANSI code page, so they never depend on the machine's. + +## Updates + +`Services/UpdateService.cs` (ported from SIC!) checks `App.AppcastUrl` with NetSparkle, and +verifies the appcast and the download with the Ed25519 public key in `App.UpdatePublicKey`, +which holds the public half of the pair in `keys/`. Should that constant ever not be a base64 +32-byte key (a fork that has not made its own pair yet), every update check is off and the log +says why; Help → Check for updates says so too. Every window is a process: only the first one +still open does the startup and background checks (the named `Local\` mutex +`UpdateService.BackgroundChecksName`), so five open plans do not offer one update five times. +Help → Check for updates works in every window. The service returns an `UpdateCheckOutcome` +and `MainWindow` announces it; the service itself shows nothing but NetSparkle's update window. + +The key pair lives in `keys/` at the repository root, which is gitignored: **never commit +`keys/NetSparkle_Ed25519.priv`**. To make it (once, or when forking): + +``` +dotnet tool install --global NetSparkleUpdater.Tools.AppCastGenerator +netsparkle-generate-appcast --generate-keys --key-path keys +``` + +then paste the contents of `keys/NetSparkle_Ed25519.pub` into `App.UpdatePublicKey`. + +## Installer and releases + +`installer/Build-Installer.ps1` (or `build-installer.bat`, which also opens the output folder) +compiles the translations with `-Strict`, publishes to `src/PlanCake/bin/x64/Release/publish`, +compiles `installer/plancake.iss` with Inno Setup 6, and writes to `installer/Output/` +(gitignored) the installer `plancake-v-setup.exe` and the portable +`plancake-v-portable.zip`, where `` is the four-part file version. +`-Appcast` adds `appcast.xml` and its signature, signed with the key in `keys/` (it needs +`netsparkle-generate-appcast`); `-Deploy` uploads the lot to plancake.oire.dev over SCP with the +host and path in `installer/deploy.json` (gitignored; copy `deploy.example.json`). Release notes +come from `changelogs/.md`, named after the tag; the script hands the file to the +generator under the four-part name it looks for. + +What ships is the same in both: `plancake.exe`, `WebView2Loader.dll`, `web\`, `help\` and +`locale\**\*.mo`. The publish folder holds more (the `.pdb`, WebView2's XML docs, a second +`WebView2Loader.dll` under `runtimes\`), so a new file beside the exe must be added to both the +`[Files]` section of `plancake.iss` and `$ShippedItems` in the script. The installer puts `{app}` +on the machine `PATH` and takes it off on uninstall, and installs the .NET 10 Desktop Runtime +and the WebView2 Runtime when missing (`CodeDependencies.iss`, from InnoDependencyInstaller). +The `AppId` in `plancake.iss` is permanent: Windows and winget know PlanCake by it. The `.iss` +and `.isl` files are UTF-8 with a BOM, which Inno Setup needs to read them as UTF-8. + +PlanCake is published to winget as `Oire.PlanCake`. The first release creates the manifest with +`wingetcreate new` on the release's installer URL; after every later GitHub release, update it: + +```bash +wingetcreate update -u 'https://github.com/Oire/plan-cake/releases/download/v/plancake-v-setup.exe|x64' -v --submit --token "$(gh auth token)" Oire.PlanCake +``` + +The installer is x64 only, but wingetcreate detects an Inno Setup installer as x86: without the +`|x64` suffix the update fails with "Multiple matches" (and in `wingetcreate new`, set the +architecture to x64 by hand). ## Error handling at startup `Program.Main` returns an `ExitCode` and installs handlers for `AppDomain.UnhandledException` and `TaskScheduler.UnobservedTaskException` — without them, an exception on a background -thread kills the process with nothing in the log. +thread kills the process with nothing in the log. For the window it also subscribes +`Application.ThreadException` (with `UnhandledExceptionMode.CatchException`): WinForms catches +an exception from the message loop itself (an `async void` handler, a `BeginInvoke` callback), +and without a handler shows its own dialog and logs nothing. The handler logs it and asks +whether to keep PlanCake open. Utility classes log and degrade; they do not show dialogs and do not call `Application.Exit`. Deciding to stop is `Program`'s job. ## Tests -xUnit + AwesomeAssertions in `tests/WinFormsTemplate.Tests`. The app project grants it +xUnit + AwesomeAssertions in `tests/PlanCake.Tests`. The app project grants it `InternalsVisibleTo`, which is how `Config.OverrideFilePath` lets the config tests write to a temp directory instead of the developer's real `%APPDATA%`. Static state means tests that touch `Config` or `Localization` must not run in parallel across classes. -Keep the existing tests when adapting the template: they are cheap, and they fail loudly if a -rename breaks the data-folder layout or the localization fallback. +- A test class that changes `Config` or the interface language, or asserts on text from `_()`, + goes in `[Collection(LocalizationCollection.Name)]`, which runs alone. +- A test that builds a form or control wraps it in `Sta.Run(...)`; forms are built and + disposed, never shown. +- `web/morph.js` and `web/blocks.js` expose pure functions that `MorphPlanTests` and + `BlockPickTests` run in Jint, so the page needs no JS toolchain. +- While a PlanCake window is open, `bin\Debug\plancake.exe` is locked: build and test with + `dotnet build --artifacts-path ` (and the same for `dotnet test`) rather than + closing the user's window. + +Keep the tests inherited from the template: they are cheap, and they fail loudly if a rename +breaks the data-folder layout or the localization fallback. diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..2e1c501 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d5bb06 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2026 Oire Software SARL, https://oire.org/ + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/WinFormsTemplate.slnx b/PlanCake.slnx similarity index 69% rename from WinFormsTemplate.slnx rename to PlanCake.slnx index c2dc270..667f337 100644 --- a/WinFormsTemplate.slnx +++ b/PlanCake.slnx @@ -3,10 +3,10 @@ - + - + diff --git a/README.md b/README.md index 896a99f..67f2894 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,176 @@ -# winforms-template +# 🥞 PlanCake -Template repository for Windows Forms applications at Oire Software. +Read Markdown files comfortably and leave notes right where they belong. -It is a working, buildable app — not a skeleton. Clone it, rename it, and start writing -features on top of a stack that already has logging, configuration, localization, tests, -versioning and CI wired together. +PlanCake is a Windows application that shows a Markdown file the way it is meant to be read: +real headings, lists, tables and code blocks instead of hash signs and asterisks. Click a +paragraph, a list item, a heading, a table row or a code block, type a note, and PlanCake writes +it straight into the `.md` file, right after that block, between a pair of markers +(`[usernote]` … `[/usernote]` by default). Whoever reads the file next, a colleague or an AI +assistant such as Claude, finds your notes exactly where they belong. -## What you get +## Why -| Area | Choice | -| --- | --- | -| Runtime | .NET 10, `net10.0-windows`, x64 | -| UI | Windows Forms, [Oire.WinForms.NativeControls](https://www.nuget.org/packages/Oire.WinForms.NativeControls) for accessible native controls | -| Logging | Serilog — full logs, a terse pair for bug reports, compact-JSON telemetry | -| Configuration | SharpConfig INI under `%APPDATA%`, or next to the EXE in portable mode | -| Localization | GetText.NET (`.po` / `.mo`), **never `.resx`** | -| Versioning | GitVersion from the tag history — no version literal anywhere | -| Tests | xUnit + [AwesomeAssertions](https://awesomeassertions.org/) (Apache-2.0 fork of FluentAssertions), running on every push | -| CI | GitHub Actions: format check, build, test | +AI coding assistants write long implementation plans, often 800 lines of Markdown and more, and +ask you to review them. Reading them raw in a code editor is tiring, and commenting on them is +worse: you either describe in a chat where each remark belongs ("in Task 4, the third item…") or +edit the file by hand and hope the assistant notices. -Analyzers run at `latest` with `TreatWarningsAsErrors`, so the build stays clean from day one. +PlanCake makes both halves comfortable. It renders the plan, so you read it like a document. And +it knows which lines of the file each rendered block came from, so leaving a note on a block +takes one click, or Enter, and no line numbers. The notes are plain text in the file itself: +there is nothing to save, no side file, and any tool that reads the file sees them. When you are +done, the assistant reads your notes with the exact blocks they refer to, reworks the plan and +removes them. -## Layout +Like every Oire application, PlanCake is built to work for everyone: everything works equally +well with the mouse, from the keyboard alone, and with a screen reader, which reads the rendered +plan with its usual heading, list and table navigation instead of spelling out Markdown +punctuation. +## Installing + +Download PlanCake from [plancake.oire.dev](https://plancake.oire.dev), in one of two forms: + +- **The installer** sets everything up: it installs the .NET 10 Desktop Runtime and the + Microsoft Edge WebView2 Runtime if they are missing, and puts `plancake` on your `PATH`. +- **The portable zip** runs from any folder. Keep the empty `userdata` folder next to + `plancake.exe`, and PlanCake keeps its settings there instead of in `%APPDATA%\Oire\PlanCake`. + +Or install it with [winget](https://learn.microsoft.com/windows/package-manager/winget/): + +```powershell +winget install Oire.PlanCake +``` + +Requirements: + +- Windows 10 or 11, x64 +- [.NET 10 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/10.0) +- [Microsoft Edge WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2/) + (preinstalled on Windows 11) + +The installer brings both runtimes along; the portable zip needs them already there. + +PlanCake speaks English, Russian, Ukrainian, French, Hebrew (with a right-to-left interface) and +German. Press F1 in PlanCake for the full user manual. + +## Reading and annotating + +- **Open a file** from the command line (`plancake plan.md`), with Ctrl+O, by dropping it on the + window, from the clipboard (Ctrl+V after copying it in File Explorer, or a copied path), or from + a link (Ctrl+L; GitHub file links work as they are). A linked file is downloaded to your + Downloads folder and opened from there, so your notes go into that copy. Each file gets its own + window, and a file is never open in two windows at once. +- **Add a note**: click a block, or move to it with Alt+Shift+Down Arrow and Alt+Shift+Up Arrow + and press Enter. Type the note and press Enter. The note is in the file at once, shown after + its block and in the notes list beside the document. Markdown in a note is shown formatted. +- **Edit, delete, move between notes**: click a note or press Enter on it to edit it; Delete in + the notes list removes one; F9 and Shift+F9 go to the next and the previous note. Ctrl+Z and + Ctrl+Y undo and redo everything PlanCake writes. +- **Tick off tasks**: task-list checkboxes (`- [ ]`) are real checkboxes; checking one writes + `[x]` on that line of the file, after a question you can turn off in Settings. +- **Keep reading while the file changes**: when something else changes the file, PlanCake + reloads it and keeps your place. It never writes a note over a change you have not seen. + +In the file, a note looks like this: + +```markdown +Back up the database before you run the migration. +[usernote]Also back up the uploads folder.[/usernote] +``` + +The markers can be changed in Settings (Ctrl+Comma), including a single marker that runs to the +end of its line. + +## The command line + +Run with a subcommand, PlanCake works without a window, for scripts and AI assistants: + +```powershell +plancake plan.md # open the window with plan.md +plancake list plan.md # print every note with the block it follows +plancake list plan.md --json -o notes.json # the same as JSON, to a file +plancake check plan.md # count the notes: exit code 0 with none, 3 with some +plancake clear plan.md # remove every note from the file +plancake export plan.md -o plan.html # a standalone web page with the notes marked +plancake check plan.md --open-marker "!NOTE!" --single-token # other markers for one run +``` + +Errors go to the error output with exit code 1. Output is UTF-8 without a byte order mark. +`plancake --help` and `plancake --help` list every option. + +PlanCake is a Windows application, so an interactive PowerShell or Command Prompt does not wait +for it: typed at the prompt, the output may appear after the prompt returns. Piped or captured +output is always complete (`plancake check plan.md | Out-Host`, or `cmd /c "plancake check +plan.md"` when you need `$LASTEXITCODE`). + +### A shorter command: `pk` + +To open plans with two letters, add an alias to your shell profile. In PowerShell (the file +`$PROFILE` names): + +```powershell +Set-Alias pk plancake ``` -├── .github/ -- CI workflow, Dependabot, release-notes categories -├── src/WinFormsTemplate/ -│ ├── Ui/ -- MainWindow and dialogs -│ ├── Utils/ -- Config, Localization -│ │ └── Constants/ -- App, Logging, ExitCode -│ └── locale/ -- .po catalogs and the gettext scripts -├── tests/WinFormsTemplate.Tests/ -├── GitVersion.yml -└── WinFormsTemplate.slnx + +In bash (`~/.bashrc`): + +```bash +alias pk=plancake +``` + +Then `pk plan.md` opens a plan, and `pk list plan.md` works like `plancake list plan.md`. + +## Reviewing a plan with Claude + +When Claude writes a plan and asks you to review it, open it with `pk plan.md`, read it, and +leave notes wherever you want changes. Tell Claude you are done: it runs +`plancake list plan.md --json` to read your notes with the blocks they refer to, reworks the +plan, removes the notes it has dealt with, and confirms with `plancake check plan.md` that none +is left. + +The [Debussy](https://debussy.oire.dev/) plugins for Claude Code have this manual-review step +built in. They find notes by the markers in their `noteMarkers` setting, where a pair is written +`open...close`, so point it at PlanCake's markers in `~/.claude/debussy.json` (or the project's +`.claude/debussy.json`): + +```json +{ + "noteMarkers": ["[usernote]...[/usernote]"] +} ``` -Single project with folder/namespace separation; namespaces follow folder names -(`Oire.WinFormsTemplate.{Folder}`). Split into more projects only when something is genuinely -reused across applications — at which point it belongs in a NuGet package, not a sibling -project. - -## Starting a new application from it - -1. Create the new repository from this template on GitHub ("Use this template"). -2. Rename, in this order: - - the directories `src/WinFormsTemplate` and `tests/WinFormsTemplate.Tests`; - - the two `.csproj` files and `WinFormsTemplate.slnx`, plus the `` and - the project paths inside the `.slnx`; - - `AssemblyName`, `RootNamespace` and `InternalsVisibleTo` in the `.csproj` files; - - the `Oire.WinFormsTemplate` namespace across the sources; - - `App.Name` in `src/.../Utils/Constants/App.cs` — this drives the data folder, the config - file name and the gettext catalog name, so nothing else needs to know the product name. -3. Set `Product` and `Description` in the `.csproj`, and the catalog name in - `locale/messages.pot`. -4. Update the `locale/scripts/Compile-Translations.ps1` path in - `.github/workflows/dotnet.yml`. -5. Delete this section of the README and write the real one. -6. Tag `v1.0.0` when the first release is ready; until then GitVersion reports `1.0.0.`. - -## Development +If you change the markers in PlanCake's settings, change them there too. + +## Building from source ```powershell +# once: gettext's msgfmt, which compiles the translations +winget install mlocati.GetText + +# before every build: .mo files are build output and are gitignored +./src/PlanCake/locale/scripts/Compile-Translations.ps1 -Strict dotnet restore dotnet build dotnet test dotnet format # --verify-no-changes is what CI runs ``` -Translations are compiled separately, since `.mo` files are build output and are gitignored: +Skip the translations step and the app falls back to English, and the localization tests fail. +CI runs the same steps in this order. + +The installer and the portable zip are built with [Inno Setup 6](https://jrsoftware.org/isinfo.php): ```powershell -./src/WinFormsTemplate/locale/scripts/Compile-Translations.ps1 +./installer/Build-Installer.ps1 ``` -See `src/WinFormsTemplate/locale/README.md` for the full translation workflow and -`CLAUDE.md` for the conventions this repository expects. +It compiles the translations, publishes a Release build and writes both to `installer/Output/`. + +See `src/PlanCake/locale/README.md` for the translation workflow and `CLAUDE.md` for the +conventions this repository follows. -## Conventions worth knowing before the first commit +## License -- **Accessibility is load-bearing.** `TableLayoutPanel` for structure, real `Label` controls - next to their inputs, and a keyboard path for every mouse interaction. Test with a screen - reader before calling any UI work done. -- **Line endings are LF everywhere** except `.bat` and `.cmd`; `.gitattributes` enforces it. -- **No version literals.** GitVersion owns `Version`, `FileVersion` and - `InformationalVersion`. -- **Every user-visible string goes through `_()`** or is set by the designer and translated by - `Localizer.Localize`. +PlanCake is licensed under the [Apache License 2.0](LICENSE). diff --git a/changelogs/1.0.0.md b/changelogs/1.0.0.md new file mode 100644 index 0000000..12fd99d --- /dev/null +++ b/changelogs/1.0.0.md @@ -0,0 +1,11 @@ +First release of PlanCake: read Markdown files comfortably and leave notes right where they belong. + +- Read Markdown as a document, with real headings, lists, tables and code blocks +- Click a block, or press Enter on it, to leave a note; it is written straight into the file, right after that block +- See your notes in the document and in a list beside it; edit, delete, jump between them with F9, and undo anything +- Check off task-list items from the document +- Open files from the command line, the Open dialog, drag and drop, the clipboard or a link +- Keeps your reading place when the file changes on disk, and never writes over a change you have not seen +- Command line for scripts and AI assistants: list, check, clear and export +- Works equally well with the mouse, the keyboard alone and a screen reader +- In English, Russian, Ukrainian, French, Hebrew and German diff --git a/docs/jaws-spike.md b/docs/jaws-spike.md new file mode 100644 index 0000000..cbc893d --- /dev/null +++ b/docs/jaws-spike.md @@ -0,0 +1,174 @@ +# JAWS spike results + +Task 2 of `docs/plans/completed/001-plan-cake-v1.md` built the WebView2 host and a throw-away page, +`src/PlanCake/web/spike.html`, to find out what JAWS does inside WebView2 before the note +triggers were built. The user ran the checklist with JAWS in the virtual cursor, with JAWS's +default settings. This file records each answer and the decision taken on it; the plan has +been updated to match. + +## 1. Enter on a block + +Enter on the second paragraph, a nested list item, a table cell, the code block and a heading +was announced with the right block and the right lines ("lines 15-15" and so on). Enter works +as a note trigger. + +Two problems came with it: + +- The reported `pointerType` was always `mouse`. By default JAWS answers Enter by emulating a + mouse click, and the user will not change that setting ("nobody will"). See item 6. +- JAWS switched to forms mode every time Enter was used on a block (not with Space). The user + found this very inconvenient. The likely cause is the permanent `tabindex="-1"` on every + block: it makes each block a focusable element, and JAWS enters forms mode when a click + lands on one. + +Decision: blocks carry no `tabindex` by default. When the page has to move the virtual cursor +(`focusLines`, `focusNote`, a new note) it sets `tabindex="-1"` on that one element, focuses +it, and removes the attribute again on blur. As a second line of defense, a `keydown` Enter +on a block or note element itself is treated like a click, so if JAWS is in forms mode anyway +(or the element has real focus), a second Enter still does something. The JAWS checks of +Tasks 6 and 7 now include "Enter on a block does not switch JAWS to forms mode". + +## 2. Applications key and Shift+F10 + +Both raise `contextmenu` in the page. They work as the context-menu trigger. + +## 3. Moving the virtual cursor from the host + +F9 made the page focus the third paragraph; JAWS read it, and Down Arrow continued from +there. `focusLines` and `focusNote` work as planned. + +## 4. Task-list checkboxes + +Nothing happens: Markdig renders them disabled, so a task cannot be toggled. Enter on them +also switches JAWS to forms mode. The user wants to be able to toggle them, possibly with a +confirmation that can be turned off in the settings. + +Decision: new scope, Task 7a in the plan. The checkboxes render enabled; toggling one asks for +confirmation (setting `ConfirmTaskToggle`, on by default), then rewrites `[ ]` or `[x]` on that +item's source line through the same stale-safe write path as notes, with undo and redo. + +## 5. Status announcements + +The UI Automation notifications raised by `StatusAnnouncer` are heard while in the document. + +Decision: the page's `announce` message is not needed and has been dropped from the page +protocol. Every announcement goes through `StatusAnnouncer`. + +## 6. Enter compared with a mouse click + +A single mouse click on the second paragraph also reported `pointerType` `mouse`, the same as +Enter. The two cannot be told apart. + +Decision: the plan's own fallback applies. Enter and a single click both activate: on a block +they add a note, on a note they edit it. A double-click does nothing extra. A click that ends +a text selection does not activate, so text can still be selected with the mouse. + +Side remark: the second paragraph's three source lines were joined into one line when +rendered. That is standard CommonMark (a line break inside a paragraph is a soft break) and +stays as it is: plans are hard-wrapped at about 100 columns, and turning soft breaks into +`
` would break up every paragraph. + +## 7. The two note styles + +- The `