diff --git a/.gitignore b/.gitignore index 8dcdae4..24576f0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ obj/ artifacts/ **/.notes/ installer/publish/ +installer/publish-macos/ +installer/publish-windows/ installer/Output/ *.user *.suo +**/.DS_Store +SampleNotes diff --git a/NoteManager.sln b/NoteManager.sln index c024283..3ff362b 100644 --- a/NoteManager.sln +++ b/NoteManager.sln @@ -1,12 +1,14 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteManager.App", "src\NoteManager.App\NoteManager.App.csproj", "{A1C64B6F-3D2E-46A2-8F7E-64218B641842}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteManager.App.Tests", "tests\NoteManager.App.Tests\NoteManager.App.Tests.csproj", "{9F829F14-BF7B-4CB4-8DE4-1C7EB40DB5C7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteManager.App.UiTests", "tests\NoteManager.App.UiTests\NoteManager.App.UiTests.csproj", "{B7961763-3072-4A43-89D2-52A5373DA9CB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40AE1BBE-BA69-42C4-AE41-B9C79190BA46}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteManager.Core", "src\NoteManager.Core\NoteManager.Core.csproj", "{C5655CB1-54E2-473E-A47A-0E8C2E4409C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoteManager.Desktop", "src\NoteManager.Desktop\NoteManager.Desktop.csproj", "{BE8B426F-D536-46C6-8297-768A80CB59F3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -14,17 +16,21 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A1C64B6F-3D2E-46A2-8F7E-64218B641842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1C64B6F-3D2E-46A2-8F7E-64218B641842}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1C64B6F-3D2E-46A2-8F7E-64218B641842}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1C64B6F-3D2E-46A2-8F7E-64218B641842}.Release|Any CPU.Build.0 = Release|Any CPU {9F829F14-BF7B-4CB4-8DE4-1C7EB40DB5C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F829F14-BF7B-4CB4-8DE4-1C7EB40DB5C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F829F14-BF7B-4CB4-8DE4-1C7EB40DB5C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F829F14-BF7B-4CB4-8DE4-1C7EB40DB5C7}.Release|Any CPU.Build.0 = Release|Any CPU - {B7961763-3072-4A43-89D2-52A5373DA9CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7961763-3072-4A43-89D2-52A5373DA9CB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7961763-3072-4A43-89D2-52A5373DA9CB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7961763-3072-4A43-89D2-52A5373DA9CB}.Release|Any CPU.Build.0 = Release|Any CPU + {C5655CB1-54E2-473E-A47A-0E8C2E4409C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5655CB1-54E2-473E-A47A-0E8C2E4409C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5655CB1-54E2-473E-A47A-0E8C2E4409C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5655CB1-54E2-473E-A47A-0E8C2E4409C4}.Release|Any CPU.Build.0 = Release|Any CPU + {BE8B426F-D536-46C6-8297-768A80CB59F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE8B426F-D536-46C6-8297-768A80CB59F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE8B426F-D536-46C6-8297-768A80CB59F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE8B426F-D536-46C6-8297-768A80CB59F3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C5655CB1-54E2-473E-A47A-0E8C2E4409C4} = {40AE1BBE-BA69-42C4-AE41-B9C79190BA46} + {BE8B426F-D536-46C6-8297-768A80CB59F3} = {40AE1BBE-BA69-42C4-AE41-B9C79190BA46} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index f6e2c90..25b78f2 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,108 @@ # NoteManager -NoteManager is a .NET 8 WPF desktop application inspired by the supplied three-pane notebook screenshot. It recreates the dense tag navigation, searchable note list, formatting toolbar, selected-note metadata, and a fully interactive in-app PDF preview. +NoteManager is a cross-platform .NET 10 desktop application built with Avalonia. +It provides tag navigation, a searchable note list, Markdown editing, note +metadata, PDF embeds, and public-link publishing on Windows and macOS. ## Run -From PowerShell in the repository root: +From the repository root on Windows or macOS: -```powershell -dotnet run --project .\src\NoteManager.App\NoteManager.App.csproj +```bash +dotnet run --project src/NoteManager.Desktop/NoteManager.Desktop.csproj ``` -The repository pins the .NET 8 SDK through `global.json`. On startup, the application recursively loads the Obsidian vault at: +The repository pins the .NET 10 SDK through `global.json`. On startup, the application recursively loads the Obsidian vault at: ```text -C:\Projects\Obsidian +SampleNotes ``` -Use **File → Open folder…** or `Ctrl+O` to switch to another Markdown folder. +Use **File → Open folder…**, `Ctrl+O` on Windows, or `Command+O` on macOS to +switch to another Markdown folder. For a dialog-free automated launch, inject the startup folder: -```powershell -dotnet run --project .\src\NoteManager.App\NoteManager.App.csproj -- --folder .\SampleNotes +```bash +dotnet run --project src/NoteManager.Desktop/NoteManager.Desktop.csproj -- \ + --folder SampleNotes ``` ## Automated regression tests -The primary UI regression lane uses -[FlaUI 5](https://github.com/FlaUI/FlaUI) with the UIA3 provider and -[NUnit 4](https://nunit.org/). It launches the real WPF executable and verifies -the application through Windows UI Automation, native dialogs, the clipboard, -filesystem outcomes, a loopback Infostacker fake, and initialized WebView2 PDF -surfaces. - -Run all service and UI tests from an unlocked interactive Windows session: +The portable service and view-model suite runs on both operating systems: -```powershell -.\tests\Run-AllTests.ps1 -Configuration Debug +```bash +dotnet test tests/NoteManager.App.Tests/NoteManager.App.Tests.csproj ``` -Run only the serialized UI suite, or filter it to one fixture: +On Windows, the executable-level search suite launches the current Avalonia +application and drives the real search box through UI Automation: ```powershell .\tests\Run-UiTests.ps1 -Configuration Debug +``` -.\tests\Run-UiTests.ps1 ` - -Filter "FullyQualifiedName~TagAssignmentUiTests" +It requires an unlocked interactive Windows session. The project remains +outside the cross-platform solution because FlaUI/UIA3 is Windows-specific. +See +[`tests/NoteManager.Desktop.UiTests/README.md`](tests/NoteManager.Desktop.UiTests/README.md) +for its user-visible search scenarios and failure artifacts. + +The former WPF/FlaUI suite remains under `tests/NoteManager.App.UiTests` only as +migration reference. + +## Package a release for team sharing + +Run the release packager from the repository root on macOS. Supply a new +three- or four-part numeric version for every release: + +```bash +./installer/package-release.sh 1.2.0 ``` -Every UI test creates a separate guarded vault below the user's temporary -folder. The data covers recursive folders, tagged and untagged notes, multiple -tag blocks, recent/all tag catalogs, Unicode and body-only searches, multiple -PDF embeds, filename collisions, publishing attachments, and a large indexing -set. The vault and its `.notes` database are removed after the scenario. +The script runs the Release test suite, publishes self-contained macOS ARM64 +and Windows x64 applications, signs the macOS application bundle, and creates: -The suite covers recursive loading and indexing; tag/search navigation; -create/delete and all automatic-save boundaries; folder switching during -background indexing; tag validation and block merging; public-link publishing -and clipboard output; multiple PDF viewers; and external-PDF copy, collision -rename, embed, save, and preview refresh. Failures attach a screen capture and -UI Automation tree below `artifacts\ui-tests`; runner `.trx` files are stored in -the same area. +```text +installer/Output/NoteManager-1.2.0-osx-arm64.zip +installer/Output/NoteManager-1.2.0-win-x64.zip +installer/Output/NoteManager-1.2.0-SHA256SUMS.txt +``` -See -[`tests\NoteManager.App.UiTests\README.md`](tests/NoteManager.App.UiTests/README.md) -for the complete regression matrix, test-vault design, runner requirements, and -guidance for adding scenarios. +Teammates can extract the appropriate archive and run `NoteManager.app` on +macOS or `NoteManager.exe` on Windows. They do not need to install .NET. + +For Intel macOS or Windows on ARM64, override the default runtime identifiers: + +```bash +./installer/package-release.sh 1.2.0 osx-x64 win-arm64 +``` -## Build the Windows installer +The script refuses to overwrite an existing release. For every subsequent +release, choose the next version, run the same command, and verify the artifacts +before sharing them: + +```bash +cd installer/Output +shasum -a 256 -c NoteManager-1.2.0-SHA256SUMS.txt +``` + +By default, the macOS app receives an ad-hoc signature suitable for internal +team sharing. A release engineer can apply an installed Developer ID +certificate by setting `NOTEMANAGER_CODESIGN_IDENTITY`: + +```bash +NOTEMANAGER_CODESIGN_IDENTITY="Developer ID Application: Example Company (TEAMID)" \ + ./installer/package-release.sh 1.2.0 +``` -Inno Setup 6 or 7 can package a self-contained, startup-optimized Release build: +Public macOS distribution additionally requires Apple's notarization process, +which is intentionally outside this internal packaging script. + +## Build platform installers + +Inno Setup 6 or 7 can package a self-contained Avalonia Release build: ```powershell .\installer\build-installer.ps1 @@ -81,11 +114,17 @@ The finished artifact is written to: installer\Output\NoteManager-1.0.0-win-x64-Setup.exe ``` -Pass `-Version 1.2.0` to version a release. The publish enables composite -ReadyToRun and disables tiered compilation, trimming, and single-file extraction -to favor predictable WPF startup. See -[`installer\README.md`](installer/README.md) for prerequisites, ARM64 builds, and -unattended installation. +Pass `-Version 1.2.0` to version a release. See +[`installer\README.md`](installer/README.md) for prerequisites, ARM64 builds, +team archives, and unattended installation. + +On macOS: + +```bash +./installer/build-macos.sh 1.0.0 osx-arm64 +``` + +Use `osx-x64` for Intel Macs. ## Included interactions @@ -93,12 +132,15 @@ unattended installation. - Browse an alphabetized tag rail whose counts are calculated from the loaded Markdown files. - Select a tag to filter the middle pane by exact tag membership. - Use the virtual **All notes** and **Untagged** tags to show the complete vault or notes without tags. -- Type in **Search notes** (or press `Ctrl+F`) to search titles, file names, tags, paths, and complete Markdown contents. +- Type in **Search notes** (or press `Ctrl+F`) to search note file names, tags, relative paths, and complete Markdown contents using strict or best-match expressions described below. - Select a note to display its original Markdown source as plain, selectable text. - Edit Markdown directly; dirty notes are saved atomically when selecting another note or view, changing folders, publishing, or closing the application. - Drag one or more PDF files onto a note row or the Markdown editor to insert Obsidian `![[...]]` embeds. PDFs dropped from outside the open folder are copied to its root and receive `(1)`, `(2)`, and later suffixes when names collide. -- View each Obsidian PDF transclusion in an interactive Edge PDF viewer below the Markdown source. -- Use the PDF viewer's page navigation, scrolling, zoom, search, text selection, outline, print, save, and full-screen controls, or click **Open PDF** to use the default desktop application. +- View each Obsidian PDF and PNG, JPG, JPEG, or BMP image transclusion beneath the Markdown source, in the same order as the embeds. Images are scaled to fit while preserving their aspect ratio. +- Use the platform PDF viewer for scrolling, zooming, selection, printing, and + saving, or click **Open PDF** to use the default desktop application. Viewer + toolbar capabilities vary by operating system; PDF text-search parity is not + a migration requirement. - Click **Share** to open a public-link panel directly beneath the main toolbar button. Publishing sends the selected note and its embedded attachments to Infostacker, then copies the returned public URL to the clipboard. - Click **Create** (or press `Ctrl+N`) to create and select an empty `Untitled note.md` in the selected folder's root. Numbered names are used when needed. - Click **Delete**, then confirm the warning, to permanently remove the selected Markdown file from disk. @@ -119,6 +161,161 @@ The update runs on every startup and whenever **File → Open folder…** is use The database uses SQLite write-ahead logging so searches can read committed batches while indexing continues. Delete the selected folder's `.notes` directory at any time to force a complete rebuild; it contains no source notes and is excluded by the repository `.gitignore`. +### How search works + +The index stores the file name, path relative to the opened folder, parsed +tags, and complete Markdown source for every note. A Unicode FTS5 index handles +word and prefix searches. A second punctuation-preserving trigram index +handles phrases, paths, email addresses, versions, and other literal text. +Matching is case-insensitive and diacritic-insensitive, so `café` can match +`cafe`. + +NoteManager waits 250 milliseconds after the latest keystroke before searching, +or you can press `Enter` to run the current expression immediately. A later +keystroke, folder change, or shutdown cancels the older query. The last +completed result remains visible while a new valid expression is running. +Malformed expressions are not executed and produce a readable status message. +A valid search with no matches clears the note list and displays **No notes +found**. While the full-text index is being built, the search box is disabled +and displays **Indexing in progress**. It becomes available when the status bar +reports **Full-text ready**. + +The selected tag, **All notes**, or **Untagged** navigation filter remains in +effect. Text search therefore searches within the current navigation scope, +not outside it. + +#### Search modes + +An unqualified query is a strict search. Strict mode requires every adjacent +term and shows the most recently modified matching note first: + +```text +project plan +all: project plan += project plan +``` + +`all:` and `=` are equivalent explicit strict-mode selectors. + +Best-match mode is selected with `best:` or `~`. Adjacent terms become +alternatives, and notes matching the most distinct terms and strongest fields +are displayed first: + +```text +best: project plan +~ project plan +``` + +Best match normally returns notes matching at least one positive term. Add `*` +to include every note in the current navigation scope and place zero-score +notes after relevant notes: + +```text +~ * project plan +``` + +#### Terms, phrases, and operators + +Letter, number, and underscore terms are prefix searches. `plan` therefore +matches `plan`, `plans`, and `planning`, with an exact word ranked above a +prefix-only match. + +Text enclosed in double quotes is one contiguous phrase: + +```text +"quarterly project plan" +``` + +Use two double quotes for a quote inside a phrase: + +```text +"the ""approved"" plan" +``` + +The supported operators are: + +| Operator | Meaning | +| --- | --- | +| `AND` | Both operands must match | +| `OR` | At least one operand must match | +| `NOT` | Exclude the following operand | +| `+term` | Require the following term or group | +| `-term` | Short form of `NOT term` | +| `( ... )` | Group an expression | +| `*` | Include the complete current scope | + +Explicit operators have the same meaning in both search modes. Operator words +are case-insensitive and are recognized only as complete, unquoted words. +`NOT` and `-` exclusions apply to the complete expression. + +Examples: + +```text +(invoice OR receipt) NOT draft +~ +invoice paid -archived +all: "project plan" AND approved +``` + +`NOT`, then `AND`, then `OR` is the operator precedence. Parentheses should be +used whenever the intended grouping would otherwise be unclear. + +#### Search specific fields + +Prefix a term or phrase with a field operator to limit where it can match: + +| Field operator | Searches | +| --- | --- | +| `name:` | Note file name | +| `tag:` | Parsed note tags | +| `path:` | Path relative to the opened folder | +| `body:` | Markdown source | + +Examples: + +```text +name:"project plan" +tag:active body:roadmap +path:Clients/Acme +~ +tag:active body:roadmap -path:archive/ +``` + +`title:` is accepted as an alias for `name:`. Only these known field names are +operators; other colon-containing text remains an ordinary search term. + +#### Literal symbols and paths + +A bare term containing punctuation is matched as a literal substring. +Forward slash and backslash are ordinary searchable characters, not operators +or escape characters: + +```text +docs/search.md +C:\Projects\NoteManager +customer@example.com +release-1.2 +``` + +The slash direction is significant. On Windows, a relative path normally uses +backslashes, while a forward-slash path can still match text in a note body. +Quote literal text when it contains spaces. + +#### Relevance and note sorting + +Best-match scoring favors file-name matches, followed by tags, relative paths, +and Markdown content. Matching more distinct positive terms provides the +largest coverage advantage; phrases, exact words, and repeated occurrences +provide additional ranking signals. Modification time is used only to break +relevance ties. + +When a valid search result is accepted, the normal Title, Created, Updated, +and Size sort selections are cleared and the sort button is disabled because +the search mode controls ordering. Strict search uses modification time +descending. Best match uses relevance descending. Clearing the search box +restores the vault's saved normal sort selection and check mark. + +The complete grammar, ordering rules, implementation design, and acceptance +criteria are documented in [`search.md`](search.md). + ## Planned background Git synchronization The planned synchronization feature assumes Git for Windows is installed and @@ -148,7 +345,7 @@ criteria, and comprehensive implementation checklist are specified in UI tests can opt into a current-user-only named pipe and change the folder in the running application without invoking the native picker: ```powershell -dotnet run --project .\src\NoteManager.App\NoteManager.App.csproj -- ` +dotnet run --project .\src\NoteManager.Desktop\NoteManager.Desktop.csproj -- ` --folder .\SampleNotes ` --automation-pipe NoteManager.UiTest @@ -160,7 +357,7 @@ dotnet run --project .\src\NoteManager.App\NoteManager.App.csproj -- ` The pipe listener is disabled unless `--automation-pipe` is explicitly supplied. Both the injected startup path and runtime commands call the same `ChangeFolderAsync` path as the production folder picker. The FlaUI suite also uses an `import-pdf|` automation command. It is -available only when the opt-in pipe is enabled, is dispatched on the WPF UI +available only when the opt-in pipe is enabled, is dispatched on the Avalonia UI thread, and calls the same PDF import path as a real drop. This makes collision, copy, embed, save, and viewer assertions deterministic without synthetic mouse input. @@ -190,7 +387,7 @@ Following the [`taskscape/InfostackerPlugin`](https://github.com/taskscape/Infos 1. Reads the selected Markdown file and prefixes it with the filename without `.md`. 2. Resolves files referenced by Obsidian `![[...]]` embeds from the selected vault. 3. Sends a multipart `POST` to `https://shr.infostacker.com/sharing/uploadmarkdownwithfiles` using the `markdown` field and repeated `files` fields. -4. Reads the returned post `id`, constructs `https://shr.infostacker.com/sharing/{id}`, and copies it to the Windows clipboard. +4. Reads the returned post `id`, constructs `https://shr.infostacker.com/sharing/{id}`, and copies it to the platform clipboard. The request is made only after the user presses the publish button. The note and combined attachments are checked against the plugin's 100 MB limit before upload. An unreadable attachment is skipped, matching the plugin behavior, while an unavailable service or rejected request is reported inside the Share panel. @@ -214,7 +411,7 @@ The complete boundary behavior is exercised against a disposable vault: .\tests\Test-AutoSave.ps1 -Configuration Debug ``` -## Markdown metadata and PDF embeds +## Markdown metadata and embedded media Tags can appear anywhere in a Markdown file using a YAML-style block: @@ -247,13 +444,14 @@ The native dialog and tag-block rewrite have a disposable-vault UI test: It verifies the recent/all repository lists, lowercase display, validation, multi-tag entry, tag removal, immediate file saving, and one-block merge. -An inline Obsidian PDF transclusion uses: +An inline Obsidian PDF or image transclusion uses: ```markdown ![[Documents/Report.pdf]] +![[assets/Pasted image 20250727091803.png]] ``` -Multiple transclusions are supported in one note. PDF targets may be absolute, relative to the note, relative to the vault root, or filename-only; filename-only links are resolved against the vault index. Viewers initialize as they approach the visible area so notes containing many PDFs remain responsive. +Multiple PDF and PNG, JPG, JPEG, or BMP transclusions are supported in one note and appear beneath the editor in Markdown order. Targets may be absolute, relative to the note, relative to the vault root, or filename-only; filename-only links are resolved against the vault media index. Changes typed into the editor refresh the previews shortly after typing stops. ## Sample documents @@ -268,19 +466,19 @@ These files contain synthetic business, research, invoice, receipt, parcel, and ## Project structure ```text +src/NoteManager.Core/ + NoteManager.Core.csproj platform-neutral models, services, and view model +src/NoteManager.Desktop/ + Controls/ Avalonia PDF viewer + Dialogs/ cross-platform tag and confirmation dialogs + MainWindow.axaml Avalonia three-pane desktop interface + Program.cs Windows/macOS application entry point src/NoteManager.App/ - Assets/ original synthetic PDF artwork - Controls/ note thumbnails and document preview UI - Infrastructure/ binding helpers and commands - Models/ tag-navigation and note models - Services/ Markdown loading, metadata/PDF parsing, SQLite FTS indexing, and sample generation - ViewModels/ folder loading, background indexing, full-text/tag filtering, and selection behavior - MainWindow.xaml high-fidelity three-pane interface + Legacy WPF UI retained as migration reference; not in the portable solution tests/ - NoteManager.App.Tests/ parser, editor, index, and PDF service tests - NoteManager.App.UiTests/ FlaUI/NUnit executable-level regression suite - Run-AllTests.ps1 serialized build, service, and UI entry point - Run-UiTests.ps1 focused UI runner with TRX and failure artifacts + NoteManager.App.Tests/ portable parser, editor, index, PDF, and view-model tests + NoteManager.Desktop.UiTests/ current Windows Avalonia search UI tests + NoteManager.App.UiTests/ legacy WPF FlaUI regression reference ``` Visual comparison evidence and the final design review are documented in `design-qa.md`. diff --git a/SampleNotes/command-template.md b/SampleNotes/command-template.md deleted file mode 100644 index cf797f6..0000000 --- a/SampleNotes/command-template.md +++ /dev/null @@ -1,15 +0,0 @@ -# Command template - -This note describes a reusable command template for support and operations work. - -```yaml -tags: - - szablon - - szablon-poleceń - - szablon-komend - - tailscale -``` - -The embedded guide should appear in an interactive PDF viewer beneath this plain Markdown source: - -![[documents/orbital-guide.pdf]] diff --git a/SampleNotes/documents/orbital-guide.pdf b/SampleNotes/documents/orbital-guide.pdf deleted file mode 100644 index 9b9ea31..0000000 Binary files a/SampleNotes/documents/orbital-guide.pdf and /dev/null differ diff --git a/SampleNotes/network-notes.md b/SampleNotes/network-notes.md deleted file mode 100644 index 22a160a..0000000 --- a/SampleNotes/network-notes.md +++ /dev/null @@ -1,10 +0,0 @@ -# Private network notes - -Use this file to collect deployment reminders and connection checks. - -tags: - - tailscale - - infrastruktura - - sieć - -The content remains plain Markdown text in the note viewer. diff --git a/SampleNotes/projects/second-template.md b/SampleNotes/projects/second-template.md deleted file mode 100644 index 89bb0c5..0000000 --- a/SampleNotes/projects/second-template.md +++ /dev/null @@ -1,10 +0,0 @@ -# Secondary template - -The loader searches subfolders recursively. - -tags: - - szablon - - dokumentacja - -This nested note verifies that shared tag counts include every Markdown file -below the selected folder. diff --git a/SampleNotes/untagged-note.md b/SampleNotes/untagged-note.md deleted file mode 100644 index 4fc44a5..0000000 --- a/SampleNotes/untagged-note.md +++ /dev/null @@ -1,4 +0,0 @@ -# An untagged note - -This file deliberately has no tag metadata. It should appear when the virtual -**Untagged** tag is selected. diff --git a/design-qa.md b/design-qa.md index a58e21e..81963a9 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,98 +1,53 @@ -# Design QA - -## Current Markdown-vault validation - -- Search-box regression capture: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\notemanager-full-text-search.png`. -- The 40 px search row and vertically centered input remove the placeholder/query clipping shown in the latest source screenshot. -- A body-only query, `loader searches subfolders`, returned `second-template.md`; those words do not occur in its file name or tags. -- The per-vault SQLite FTS5 index was created at `.notes\search.db`, reported progress without blocking the window, and reached **Full-text ready**. -- Switching folders during the initial 8,087-note Obsidian indexing pass cancelled the superseded pass and completed the new vault index while the window remained responsive. -- `tests\Test-FolderInjection.ps1` passed: the app started from an injected four-note path, changed to the nested one-note path over the opt-in current-user pipe, opened no folder dialog, and returned the note for a body-only search. -- `tests\Test-ToolbarActions.ps1` passed against a disposable vault: Share opened directly beneath its toolbar button, Create wrote a zero-byte root Markdown file, declining Delete preserved it, and confirming Delete permanently removed it. -- `tests\Test-InfostackerPublishing.ps1` passed against a local mock server: the panel showed no access list, the multipart endpoint/fields/Markdown/attachment matched `taskscape/InfostackerPlugin`, and the returned public URL was copied to the Windows clipboard. -- `tests\Test-AutoSave.ps1` passed against a disposable vault: edits were persisted on note selection, view selection, and window close, with no atomic-save temporary files left behind. -- Current implementation capture: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\notemanager-final.png`. -- Startup recursively loaded 8,087 Markdown files from `C:\Projects\Obsidian`. -- **File → Open folder…** was exercised with `SampleNotes` and loaded all 4 Markdown files, including the nested `projects` folder. -- The sample `szablon` tag filtered the middle list to 2 notes; virtual `Untagged` filtered it to 1 note; `All notes` restored all 4. -- The selected note exposed its unchanged plain Markdown text and recognized `![[documents/orbital-guide.pdf]]`. -- A real Obsidian note with two PDF transclusions produced 2 resolved interactive viewers. -- Repeatedly switching between one- and two-PDF notes remained responsive. -- The Edge PDF toolbar visibly provides outline, page selection, zoom, fit, rotate, search, print, save, full-screen, and settings controls. -- The release build completed with zero warnings and zero errors. - -## Baseline fidelity findings - -The final comparison has no actionable P0, P1, or P2 mismatches. The requested hierarchy is present and legible: counted tags on the left, notes in the middle, and the selected note with a direct two-page PDF preview on the right. +# Design QA — unified Markdown editor surface ## Comparison target -- Source visual truth: `C:\Users\TASKSC~1\AppData\Local\Temp\codex-clipboard-c47fe6bb-2370-4b7e-ab97-6da4749894e6.png` -- Implementation screenshot: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\notemanager-updated-final.png` -- Source pixels: 3840 × 2304. -- Raw implementation pixels: 3866 × 2330, including the 13 px maximized-window shadow on every edge. -- Normalized implementation pixels: 3840 × 2304 after cropping the 13 px shadow. No density resampling was required. -- Application design viewport at the time of this baseline capture: 1920 × 1130 rendered at the active 200% Windows scale. -- Baseline state: maximized native window; no tag filter; 13 synthetic notes; `img20230118_12404076.pdf` selected. +- Source visual truth: `/Users/mzag/NoteManager/artifacts/contextual-editor-actions-final.png` +- User-directed change: remove the “MARKDOWN” label and contain Tags, the Markdown editor, and Import PDF within one expanded white surface. +- Final implementation: `/Users/mzag/NoteManager/artifacts/unified-editor-surface.png` +- Full-view comparison: `/Users/mzag/NoteManager/artifacts/unified-editor-surface-comparison.png` +- Focused editor comparison: `/Users/mzag/NoteManager/artifacts/unified-editor-surface-focus.png` +- State: macOS light theme, SampleNotes loaded, All notes and `untagged-note.md` selected, editor unfocused. -## Evidence +## Viewport and normalization -- Full-view comparison: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\comparison-updated-final-full.png` -- Focused tag and note-list comparison: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\comparison-updated-final-tags-list.png` -- Focused editor and PDF comparison: `C:\Users\TaskscapeLtd\Documents\NoteManager\artifacts\comparison-updated-final-editor.png` +- Source pixels: 1312 × 768. +- Implementation pixels: 1312 × 768. +- Native desktop capture; browser CSS size and `deviceScaleFactor` do not apply. +- Both captures use the same application state, window size, content, theme, and density. +- Full comparison: 3040 × 1040 Retina PNG with undistorted captures in equal slots. +- Focused comparison: 2200 × 1360 Retina PNG with matching aspect-preserved editor-pane crops. -The focused comparisons are required because tag counts, note metadata, the selected-row border, toolbar icons, tag pills, PDF page margins, and generated document detail are too small to judge reliably from the full view alone. +## Findings -## Fidelity review +No actionable P0, P1, or P2 differences remain. -- Fonts and typography: Segoe UI matches the dense Windows desktop interface. Final title, tag, metadata, count, toolbar, and date sizes preserve the source hierarchy, clipping, and ellipsis behavior. -- Spacing and layout rhythm: the 300 / 1 / 550 / 1 / flexible WPF tracks normalize to the source pane boundaries. The header, search field, 90 px note rows, editor toolbar, PDF canvas, page inset, and scrollbar positions align closely without hidden persistent controls. -- Colors and visual tokens: light-gray rails, white note surfaces, subdued blue-gray metadata, light-blue tag chips, green New Note action, cyan selection border, and charcoal PDF canvas follow the source palette and state treatments. -- Image quality and asset fidelity: the selected note uses two project-bound, high-resolution generated raster pages with the source's monochrome photocopied construction-manual art direction. The cover remains sharp in both its 151 × 80 thumbnail and large PDF view. No placeholder or code-drawn document artwork is used. -- Copy and content: filenames, tags, notebooks, dates, counts, and document copy are coherent synthetic data. `Blocki`, `Manual`, and `Stacja kosmiczna` match the selected-note state in the source. -- Icons: the visible app actions use the closest built-in Segoe MDL2 equivalents, consistently sized and aligned. Document artwork is raster rather than icon approximations. -- Responsiveness and accessibility: the current application uses responsive 300 / 1 / 550 / 1 / flexible pane tracks without a whole-window transform, text does not clip, list regions scroll independently, primary controls expose automation names, and keyboard shortcuts remain available. +- Typography: the redundant uppercase “MARKDOWN” label is removed. Existing title, content, action-label, weight, wrapping, and line-height treatments remain unchanged. +- Spacing and layout: one padded white surface now contains the top-right Tags action, the inset Markdown editor boundary, and the bottom-right Import PDF action. All controls remain visible at 1312 × 768. +- Colors and tokens: the outer surface uses the existing panel background, border, and corner-radius tokens. The inner editor retains its white background and subtle boundary, creating a clear nested hierarchy without introducing a new color. +- Asset fidelity: Tags and Import PDF continue using 16 px Fluent UI System Icons; no custom graphics or placeholder assets were added. +- Copy and content: action labels and tooltips are unchanged; only the redundant section label was removed. +- Interaction hierarchy: the expanded outer boundary makes the two actions read as utilities belonging to the editor rather than independent page actions. +- Accessibility: both actions retain visible labels, tooltips, disabled states, and keyboard-focus behavior. -## Interaction checks - -Native UI Automation checks passed: +## Focused evidence -- selecting the `Manual` tag reduced the middle pane to 1 note and kept `img20230118_12404076.pdf` selected; -- activating the Tags heading cleared the tag filter and restored all 13 notes; -- entering `energy` in Search notes returned the one energy-report result; -- creating a note increased the count to 14, selected `Untitled note`, and added the `draft` tag to the counted tag rail. - -The draft is intentionally in-memory, so restarting the app restored the 13-note reference state for the final capture. Browser and browser-console checks do not apply to this native WPF application. +The focused comparison clearly shows the requested structural change: the former separate controls are now contained within one continuous white surface, while the Markdown editor retains an identifiable internal boundary. Right alignment and approximate vertical positions are preserved. ## Comparison history -### Pass 1 - -- [P2] Tag counts were aligned to the far edge of the rail rather than immediately following their labels. -- [P2] Single-attachment rows displayed generic `1 attachment` copy instead of the filename and size visible in the updated source. -- [P2] The PDF viewer had a compressed top inset and borderline horizontal overflow, making the page sit too high compared with the source. -- Fixes: changed tag rows to inline label/count layout; added filename-aware list metadata; tightened the PDF viewer width; and matched the dark-canvas and white-page top spacing. -- Evidence: `comparison-updated-full.png`, `comparison-updated-tags-list.png`, and `comparison-updated-editor.png`. +- First formal comparison passed. No P0, P1, or P2 fixes were required after the final implementation capture. -### Pass 2 - -- Evidence: the three `comparison-updated-final-*` files listed above. -- Result: the earlier P2 differences are resolved. There are no remaining actionable P0, P1, or P2 findings. - -## Implementation checklist +## Interaction checks -- [x] Generate tag rows and counts from note data. -- [x] Filter the note list by exact tag and restore all notes from the Tags heading. -- [x] Keep search, note selection, new-note creation, share, and sync interactions operational. -- [x] Show a thin cyan outline around the selected note. -- [x] Open the selected space-manual note directly into a scrollable multi-page PDF preview. -- [x] Verify Debug and Release builds with zero warnings and zero errors. +- Tags opened the existing Assign Tags dialog and Cancel returned cleanly to the unified editor. +- Import PDF opened the native PDF picker and Cancel returned cleanly to the unified editor. +- The application built with zero warnings or errors. +- All 16 automated tests passed. -## Follow-up polish +## Residual P3 / test gaps -- [P3] The application name, sample account text, and native executable icon intentionally differ from the proprietary reference branding. -- [P3] The generated orbital manual is a new synthetic asset rather than a copy of the source document. -- [P3] Some formatting glyphs are the nearest built-in Windows equivalents rather than the original proprietary icon set. +- Windows and dark-theme rendering were not visually captured in this macOS environment; the implementation uses existing cross-platform controls and light/dark design tokens. ## Final result diff --git a/global.json b/global.json index 9471ab5..6d5813f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.120", + "version": "10.0.302", "rollForward": "latestPatch", "allowPrerelease": false } diff --git a/installer/README.md b/installer/README.md index 8057475..0694aa8 100644 --- a/installer/README.md +++ b/installer/README.md @@ -1,18 +1,55 @@ # NoteManager Installer -This folder publishes the .NET 8 WPF application and packages the complete -publish output as an Inno Setup installer. +This folder packages the cross-platform .NET 10 Avalonia application for +Windows and macOS. ## Prerequisites -- The .NET 8 SDK selected by the repository `global.json`. +- The .NET 10 SDK selected by the repository `global.json`. +- macOS, including the built-in `ditto`, `codesign`, and `shasum` tools, to + create the cross-platform team-sharing archives. - Inno Setup 6 or 7. The script searches `PATH` and the standard 32-bit and - 64-bit installation folders, or accepts an explicit `-IsccPath`. -- The Microsoft Edge WebView2 Evergreen Runtime on the target computer. It is - normally present on Windows 11; it can be obtained from the - [official WebView2 download page](https://developer.microsoft.com/microsoft-edge/webview2/). + 64-bit installation folders, or accepts an explicit `-IsccPath`. Inno Setup + is required only for the Windows setup executable. +- The platform web view used by Avalonia (WebView2 on Windows and WKWebView on + macOS) for embedded PDF display. +- macOS 14 or newer for macOS builds, matching the supported .NET 10 runtime + platforms. -## Build +## Package a release for team sharing + +From the repository root on macOS, pass the release version: + +```bash +./installer/package-release.sh 1.2.0 +``` + +The script tests the solution, cross-publishes self-contained macOS ARM64 and +Windows x64 payloads, creates versioned ZIP archives, and writes a SHA-256 +checksum manifest under `installer/Output`. Existing artifacts are never +overwritten. + +Override the architecture defaults when needed: + +```bash +./installer/package-release.sh 1.2.0 osx-x64 win-arm64 +``` + +Verify the finished archives before sharing them: + +```bash +cd installer/Output +shasum -a 256 -c NoteManager-1.2.0-SHA256SUMS.txt +``` + +The macOS archive is signed ad hoc by default. Set +`NOTEMANAGER_CODESIGN_IDENTITY` to the name of an installed Developer ID +Application certificate when a real signing identity is available. Public +distribution also requires notarization. + +## Build platform installers + +### Windows From the repository root: @@ -51,12 +88,27 @@ The build script favors cold startup over output size: - precompiles managed assemblies with ReadyToRun and composite ReadyToRun; - disables tiered compilation so startup does not wait for later JIT tiers; - keeps the application folder-based, avoiding single-file extraction at launch; -- disables trimming because WPF and WebView2 use runtime-discovered types; +- disables trimming because Avalonia and native web views use + runtime-discovered types; - omits debug symbols and creates a deterministic release build. The resulting installer is larger than a framework-dependent or trimmed build, but the target computer does not need a separately installed .NET runtime. +### macOS + +From macOS, build a self-contained application bundle for the current +architecture: + +```bash +./installer/build-macos.sh 1.0.0 osx-arm64 +``` + +Use `osx-x64` for Intel Macs. The application bundle is written to +`installer/Output/NoteManager--.app`. Set +`NOTEMANAGER_CODESIGN_IDENTITY` to sign the bundle during the build; otherwise +an ad-hoc signature is applied when `codesign` is available. + ## Install Run the generated setup executable. It installs NoteManager for all users under diff --git a/installer/build-installer.ps1 b/installer/build-installer.ps1 index 15552e3..493614e 100644 --- a/installer/build-installer.ps1 +++ b/installer/build-installer.ps1 @@ -3,7 +3,7 @@ Publishes NoteManager and builds an Inno Setup installer. .DESCRIPTION - Creates an optimized, self-contained Windows publish of the WPF application + Creates an optimized, self-contained Windows publish of the Avalonia application and compiles it into a versioned Inno Setup installer. Startup-oriented publish settings are enabled by default: @@ -51,7 +51,7 @@ $ErrorActionPreference = "Stop" $scriptDir = [System.IO.Path]::GetFullPath($PSScriptRoot) $repoRoot = [System.IO.Path]::GetFullPath((Join-Path $scriptDir "..")) -$projectPath = Join-Path $repoRoot "src\NoteManager.App\NoteManager.App.csproj" +$projectPath = Join-Path $repoRoot "src\NoteManager.Desktop\NoteManager.Desktop.csproj" $publishDir = Join-Path $scriptDir "publish" $outputDir = Join-Path $scriptDir "Output" $issFile = Join-Path $scriptDir "NoteManager.iss" diff --git a/installer/build-macos.sh b/installer/build-macos.sh new file mode 100755 index 0000000..e27eb3c --- /dev/null +++ b/installer/build-macos.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +version="${1:-1.0.0}" +runtime="${2:-osx-arm64}" + +case "$runtime" in + osx-arm64|osx-x64) ;; + *) + echo "Unsupported runtime '$runtime'. Use osx-arm64 or osx-x64." >&2 + exit 2 + ;; +esac + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +project="$repo_root/src/NoteManager.Desktop/NoteManager.Desktop.csproj" +publish_dir="$script_dir/publish-macos/$runtime" +app_dir="$script_dir/Output/NoteManager-$version-$runtime.app" +contents_dir="$app_dir/Contents" + +mkdir -p "$publish_dir" "$script_dir/Output" + +dotnet publish "$project" \ + --nologo \ + -c Release \ + -r "$runtime" \ + --self-contained true \ + -o "$publish_dir" \ + -p:Version="$version" \ + -p:UseAppHost=true \ + -p:PublishSingleFile=false \ + -p:PublishTrimmed=false \ + -p:DebugSymbols=false \ + -p:DebugType=None + +if [[ ! -x "$publish_dir/NoteManager" ]]; then + echo "Publish did not produce the NoteManager executable." >&2 + exit 1 +fi + +if [[ -e "$app_dir" ]]; then + echo "Output already exists: $app_dir" >&2 + echo "Move or remove it before rebuilding." >&2 + exit 1 +fi + +mkdir -p "$contents_dir/MacOS" "$contents_dir/Resources" +cp -R "$publish_dir/." "$contents_dir/MacOS/" +sed "s/__VERSION__/$version/g" \ + "$script_dir/macos/Info.plist" > "$contents_dir/Info.plist" + +if command -v codesign >/dev/null 2>&1; then + codesign --force --deep --sign \ + "${NOTEMANAGER_CODESIGN_IDENTITY:--}" "$app_dir" +fi + +echo "Application bundle created: $app_dir" diff --git a/installer/macos/Info.plist b/installer/macos/Info.plist new file mode 100644 index 0000000..ccb98a1 --- /dev/null +++ b/installer/macos/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + NoteManager + CFBundleExecutable + NoteManager + CFBundleIdentifier + com.taskscape.notemanager + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + NoteManager + CFBundlePackageType + APPL + CFBundleShortVersionString + __VERSION__ + CFBundleVersion + __VERSION__ + LSMinimumSystemVersion + 14.0 + NSHighResolutionCapable + + + diff --git a/installer/package-release.sh b/installer/package-release.sh new file mode 100755 index 0000000..870ce3a --- /dev/null +++ b/installer/package-release.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + ./installer/package-release.sh [mac-runtime] [windows-runtime] + +Examples: + ./installer/package-release.sh 1.2.0 + ./installer/package-release.sh 1.2.0 osx-x64 win-arm64 + +Defaults: + mac-runtime osx-arm64 + windows-runtime win-x64 +EOF +} + +if [[ $# -lt 1 || $# -gt 3 ]]; then + usage >&2 + exit 2 +fi + +version="$1" +mac_runtime="${2:-osx-arm64}" +windows_runtime="${3:-win-x64}" + +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "Version must contain three or four numeric components, for example 1.2.0." >&2 + exit 2 +fi + +case "$mac_runtime" in + osx-arm64|osx-x64) ;; + *) + echo "Unsupported macOS runtime '$mac_runtime'. Use osx-arm64 or osx-x64." >&2 + exit 2 + ;; +esac + +case "$windows_runtime" in + win-x64|win-arm64) ;; + *) + echo "Unsupported Windows runtime '$windows_runtime'. Use win-x64 or win-arm64." >&2 + exit 2 + ;; +esac + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This script must run on macOS so it can create and sign the application bundle." >&2 + exit 1 +fi + +for command_name in dotnet ditto codesign shasum; do + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Required command is not available: $command_name" >&2 + exit 1 + fi +done + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/.." && pwd)" +project="$repo_root/src/NoteManager.Desktop/NoteManager.Desktop.csproj" +solution="$repo_root/NoteManager.sln" +plist_template="$script_dir/macos/Info.plist" +output_dir="$script_dir/Output" + +mac_archive="$output_dir/NoteManager-$version-$mac_runtime.zip" +windows_archive="$output_dir/NoteManager-$version-$windows_runtime.zip" +checksum_file="$output_dir/NoteManager-$version-SHA256SUMS.txt" + +for output_path in "$mac_archive" "$windows_archive" "$checksum_file"; do + if [[ -e "$output_path" ]]; then + echo "Output already exists: $output_path" >&2 + echo "Use a new release version or move the existing artifact before packaging." >&2 + exit 1 + fi +done + +staging_dir="$(mktemp -d "${TMPDIR:-/tmp}/notemanager-release.XXXXXX")" +cleanup() { + rm -rf "$staging_dir" +} +trap cleanup EXIT + +mac_publish_dir="$staging_dir/publish-$mac_runtime" +windows_publish_dir="$staging_dir/publish-$windows_runtime" +mac_app="$staging_dir/NoteManager-$version-$mac_runtime.app" +windows_folder="$staging_dir/NoteManager-$version-$windows_runtime" + +version_parts=(${version//./ }) +while [[ ${#version_parts[@]} -lt 4 ]]; do + version_parts+=("0") +done +version_info="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}.${version_parts[3]}" + +publish_application() { + local runtime="$1" + local destination="$2" + + dotnet publish "$project" \ + --nologo \ + -c Release \ + -r "$runtime" \ + --self-contained true \ + -o "$destination" \ + -p:Version="$version" \ + -p:FileVersion="$version_info" \ + -p:AssemblyVersion="$version_info" \ + -p:UseAppHost=true \ + -p:PublishSingleFile=false \ + -p:PublishTrimmed=false \ + -p:DebugSymbols=false \ + -p:DebugType=None \ + -p:ContinuousIntegrationBuild=true \ + -p:Deterministic=true +} + +echo "Running release tests..." +dotnet test "$solution" -c Release --nologo + +echo +echo "Publishing NoteManager for $mac_runtime..." +publish_application "$mac_runtime" "$mac_publish_dir" + +if [[ ! -x "$mac_publish_dir/NoteManager" ]]; then + echo "The macOS publish did not produce an executable NoteManager app host." >&2 + exit 1 +fi + +mkdir -p "$mac_app/Contents/MacOS" "$mac_app/Contents/Resources" +cp -R "$mac_publish_dir/." "$mac_app/Contents/MacOS/" +sed "s/__VERSION__/$version/g" \ + "$plist_template" > "$mac_app/Contents/Info.plist" + +codesign_identity="${NOTEMANAGER_CODESIGN_IDENTITY:--}" +codesign --force --deep --sign "$codesign_identity" "$mac_app" + +echo +echo "Publishing NoteManager for $windows_runtime..." +publish_application "$windows_runtime" "$windows_publish_dir" + +if [[ ! -f "$windows_publish_dir/NoteManager.exe" ]]; then + echo "The Windows publish did not produce NoteManager.exe." >&2 + exit 1 +fi + +mkdir -p "$windows_folder" +cp -R "$windows_publish_dir/." "$windows_folder/" +mkdir -p "$output_dir" + +echo +echo "Creating team-sharing archives..." +ditto -c -k --sequesterRsrc --keepParent "$mac_app" "$mac_archive" +ditto -c -k --norsrc --keepParent "$windows_folder" "$windows_archive" + +( + cd "$output_dir" + shasum -a 256 \ + "$(basename "$mac_archive")" \ + "$(basename "$windows_archive")" > "$(basename "$checksum_file")" +) + +echo +echo "Release package created successfully:" +du -h "$mac_archive" "$windows_archive" "$checksum_file" diff --git a/search.md b/search.md new file mode 100644 index 0000000..82ce421 --- /dev/null +++ b/search.md @@ -0,0 +1,462 @@ +# Full-text search specification + +## Status + +This document describes the implemented NoteManager full-text search system. +Search is operated entirely through expressions entered in the existing +**Search notes** box. There is no advanced-search window or query-builder UI. + +The implementation provides: + +- strict and best-match modes; +- words, phrases, grouping, and Boolean operators; +- required and excluded operands; +- file-name, tag, relative-path, and body field operators; +- literal punctuation, including `/` and `\`; +- deterministic search-owned ordering; +- a Unicode word index and a punctuation-preserving literal index. + +## Design principles + +The query language follows these rules: + +1. A query without a mode selector behaves like the previous strict search. +2. Explicit operators have the same meaning in every mode. +3. The mode changes the implicit relationship between adjacent terms and the + result ordering. +4. Unrelated notes are not best-match results unless the user explicitly adds + the match-all operator. +5. The search expression is the complete and only search configuration. +6. Invalid and stale queries never replace a completed result. + +## Search scope + +Searchable fields are: + +1. note file name; +2. parsed tags; +3. path relative to the opened vault; +4. complete Markdown source. + +The absolute vault path is not indexed. Search is applied inside the selected +tag, **All notes**, or **Untagged** navigation scope. + +## Query modes + +A mode selector is recognized only at the beginning of the trimmed search +text. Keyword selectors are case-insensitive. + +| Mode | Keyword | Symbol | Adjacent terms | Ordering | +| --- | --- | --- | --- | --- | +| Strict | `all:` | `=` | Implicit `AND` | Modified descending | +| Best match | `best:` | `~` | Implicit `OR` | Relevance descending | + +Strict is the default: + +```text +project plan +all: project plan += project plan +``` + +All three expressions require both `project` and `plan`. + +Best-match examples: + +```text +best: project plan +~ project plan +``` + +These expressions return notes matching `project` or `plan`, with notes +matching both normally ranked first. + +The symbol may touch the expression, so `=project` and `~project` are valid. +A keyword selector must include its colon. `all` and `best` without a colon +are ordinary terms. + +A selector without an expression is an empty search. It does not activate +search ordering or clear the normal sort selection. + +## Terms + +### Word and prefix terms + +A bare term containing only Unicode letters, numbers, or underscores is a +word-prefix search: + +```text +plan +``` + +It can match `plan`, `plans`, or `planning`. An exact word receives a ranking +bonus over a prefix-only match. + +Matching is case-insensitive. Text and queries are normalized consistently for +diacritics and whitespace. + +### Literal punctuation + +A bare term containing other punctuation is a literal substring: + +```text +docs/search.md +C:\Projects\NoteManager +customer@example.com +release-1.2 +#planning +``` + +Every punctuation character must be present in the indexed field. `/` is not +a regular-expression delimiter, and `\` is not an escape character. Slash +direction is significant. + +A leading `-` is the exclusion operator. To search for literal text beginning +with a dash, place it in double quotes. + +### Quoted phrases + +Double quotes create one contiguous phrase, including spaces: + +```text +"quarterly project plan" +"C:\Shared Notes\roadmap.md" +``` + +The phrase must occur inside one indexed field. It cannot start in the file +name and finish in the body. Runs of whitespace are normalized to one space; +other punctuation remains significant. + +Backslash is always literal. A double quote inside a phrase is written as two +double quotes: + +```text +"the ""approved"" plan" +``` + +An unmatched or empty quoted phrase is a syntax error. + +## Operators + +### Boolean and modifier operators + +| Operator | Meaning | +| --- | --- | +| `AND` | Both operands must match | +| `OR` | At least one operand must match | +| `NOT` | Exclude the following operand | +| `+operand` | Require the following term, phrase, or group | +| `-operand` | Short form of `NOT operand` | +| `( ... )` | Group an expression | +| `*` | Match the complete current navigation scope | + +`AND`, `OR`, and `NOT` are case-insensitive only when they are complete, +unquoted words. `"AND"` searches for the word as a phrase, and `not-ready` +remains a literal term. + +Operator precedence is: + +1. `NOT`, `+`, and `-`; +2. `AND`; +3. `OR`. + +Parentheses override precedence: + +```text +(invoice OR receipt) NOT draft +all: "project plan" AND approved +``` + +Required and excluded operands apply to the complete result: + +```text +~ +invoice paid -archived +``` + +This requires `invoice`, uses `paid` as an additional ranking signal, and +excludes every note matching `archived`. + +Best-match adjacency is an implicit `OR`, but an explicit `AND` still requires +both sides: + +```text +~ invoice paid +~ invoice AND paid +``` + +The first expression accepts either term. The second accepts only notes +matching both. + +### Match-all operator + +Best match normally returns notes matching at least one positive term. The +`*` operator explicitly includes every note in the current navigation scope: + +```text +~ * project plan +``` + +Notes matching `project` or `plan` are ranked first. Notes matching neither +remain at the end with zero relevance. This is the only implicit way to retain +unrelated notes in a best-match result. + +`* NOT archived` returns the complete current scope except notes matching +`archived`. + +## Field operators + +| Operator | Indexed field | +| --- | --- | +| `name:` | Note file name | +| `title:` | Alias for `name:` | +| `tag:` | Parsed tags | +| `path:` | Vault-relative path | +| `body:` | Markdown source | + +A field operator accepts one word, literal term, or quoted phrase: + +```text +name:roadmap +name:"project plan" +tag:active +path:Clients/Acme +path:"Clients\Acme Notes" +body:"approved budget" +``` + +Field operands combine with every other operator: + +```text +~ +tag:active body:roadmap -path:archive/ +(name:invoice OR tag:receipt) NOT body:draft +``` + +Only the known field names above are parsed as field operators. Other +colon-containing values, such as `https://example.com`, are literal terms. +A field operator without an operand is a syntax error. + +## Strict mode + +Strict mode returns only notes for which the complete expression is true. +Adjacent terms are implicitly joined with `AND`. + +Examples: + +| Query | Required result | +| --- | --- | +| `alpha beta` | Both terms | +| `"alpha beta"` | The contiguous phrase | +| `alpha OR beta` | Either term | +| `alpha NOT beta` | `alpha` without `beta` | +| `path:docs\search.md` | Literal relative path | + +Results are ordered by: + +1. file modification time descending; +2. file name ascending, case-insensitive; +3. relative path ascending, case-insensitive. + +Relevance does not change strict ordering. + +## Best-match mode + +Best match returns notes satisfying the positive expression and all required +and excluded operands. Adjacent positive terms are implicitly joined with +`OR`. + +Ranking considers: + +- number of distinct positive terms matched; +- weighted field matches; +- FTS5 BM25 relevance; +- exact-word matches over prefix-only matches; +- quoted phrases and punctuation-preserving literal matches; +- repeated occurrences with bounded frequency contribution; +- coverage of explicit `AND` groups. + +Initial field weights are: + +| Field | Weight | +| --- | ---: | +| File name | 6 | +| Tags | 4 | +| Relative path | 2 | +| Markdown content | 1 | + +Each distinct positive term supplies a coverage bonus before field-level +signals are compared. This makes a note matching more of the requested terms +normally outrank a note with one strong field match. + +Results are ordered by: + +1. relevance score descending; +2. distinct positive term count descending; +3. file modification time descending; +4. file name ascending, case-insensitive; +5. relative path ascending, case-insensitive. + +Modification time is a tie breaker, not part of relevance. + +## Search and normal sorting + +The persisted normal sort and active search order are separate states: + +- `SelectedSortType` remains the preferred Title, Created, Updated, or Size + sort for the vault; +- an accepted non-empty result activates strict or best-match search order. + +When the latest valid search result is accepted: + +1. all normal sort-menu checkmarks are cleared; +2. the sort button is disabled; +3. its tooltip explains that search controls note order; +4. strict or best-match ordering is applied; +5. the persisted normal sort preference remains unchanged. + +Clearing the search box cancels pending work, deactivates search ordering, +restores the preferred sort, and restores its checkmark. + +An incomplete or invalid expression does not clear the normal sort and does +not replace the last accepted search result. + +## Search-box interaction + +Typing starts a 250 millisecond debounce so search updates without requiring a +button. Pressing `Enter` cancels that wait and submits the current text +immediately. Enter is a submission action only and is not part of the search +grammar. + +Search is unavailable until the current folder's index is complete. During an +index build, the search box is disabled and its placeholder reads **Indexing in +progress**. The box is enabled with its normal **Search notes** placeholder only +after the status becomes **Full-text ready**. If indexing fails, it remains +disabled and reads **Search unavailable**. + +An accepted search with zero matches displays an empty note list and a visible +**No notes found** message. Its status remains a normal successful search +status, such as `Strict search · 0 notes`; it is not reported as a parser or +index error. + +## Parser and errors + +`NoteSearchQueryParser` produces a typed expression tree. Raw user input is +never passed directly to FTS5 syntax. + +The parser: + +- removes only an initial mode selector; +- retains literal punctuation; +- distinguishes terms, phrases, fields, groups, and operators; +- records required and excluded expressions separately; +- inserts the mode-specific implicit operator; +- normalizes literal values; +- returns a readable error and character position. + +Errors include: + +- unmatched or empty quotes; +- empty groups; +- missing closing parentheses; +- leading or trailing binary operators; +- missing field operands. + +The application retains the last completed result while showing the error in +the status area. + +## Index design + +The per-vault database remains: + +```text +\.notes\search.db +``` + +Schema version 2 contains: + +- `indexed_notes`, holding identity and file metadata; +- `note_search`, using `unicode61 remove_diacritics 2` for word-prefix and + BM25 searches; +- `note_literal_search`, using the FTS5 trigram tokenizer over normalized + fields for phrases and punctuation-preserving substring searches. + +Literal values containing `%` or `_` use exact `instr` verification so those +characters never become SQL wildcard operators. + +When an older disposable index is opened, the search tables and index metadata +are rebuilt. Source Markdown notes are never modified or deleted by migration. + +## Ordered result contract + +The service returns ordered `NoteSearchHit` values containing: + +```text +Path +Name +RelativePath +RelevanceScore +MatchedPositiveTermCount +ModifiedUtcTicks +``` + +The view model retains hit scores and counts rather than converting results to +an unordered set. It intersects hit membership with the active navigation +scope and reapplies the documented deterministic ordering. + +## Responsiveness and stale-result protection + +Search retains the 250-millisecond debounce. Each operation is guarded by: + +- opened-folder generation; +- exact search-box text; +- monotonically increasing search generation; +- cancellation token. + +Only the latest matching generation may update note membership, selection, +status, or sort state. + +Index updates commit batches of 200 notes through SQLite WAL. The search box +remains disabled for the complete update so the interface never accepts a +query against a partial index. A previously entered expression is rerun after +indexing completes. + +## Tests and acceptance criteria + +Automated coverage verifies: + +- all mode selector forms and mode-only input; +- strict implicit `AND` and best-match implicit `OR`; +- `AND`, `OR`, `NOT`, `+`, `-`, `*`, and grouping; +- global required and excluded operands; +- quoted phrases and syntax errors; +- literal slash and backslash matching; +- `name:`, `title:`, `tag:`, `path:`, and `body:`; +- case and diacritic normalization; +- strict modification-time ordering; +- best-match coverage and field ranking; +- explicit match-all zero-score ordering; +- tag navigation combined with search; +- physical typing and Enter submission; +- disabled search and indexing placeholder until the index is ready; +- the visible zero-result empty state; +- clearing and restoring normal sort state; +- cancellation and partial-index behavior. + +The implementation is accepted when: + +1. unqualified, `all:`, and `=` strict searches require every adjacent term; +2. `best:` and `~` use any adjacent positive term and preserve relevance + order; +3. `+` requires and `NOT` / `-` excludes across the complete result; +4. `*` explicitly includes the complete current navigation scope; +5. phrases and literal punctuation differ from separated word queries; +6. field operators restrict both membership and scoring; +7. strict results are newest-modified first; +8. best-match results are strongest first with deterministic ties; +9. accepted searches clear and disable normal sorting; +10. clearing search restores the persisted sort; +11. invalid and stale queries cannot replace a valid result; +12. Enter submits the current search immediately; +13. a successful zero-match search shows an empty list and **No notes found**; +14. indexing disables search and displays **Indexing in progress** until ready; +15. the portable and executable UI test suites and solution build pass without + warnings. diff --git a/spikes/AvaloniaPdfSpike/App.axaml b/spikes/AvaloniaPdfSpike/App.axaml new file mode 100644 index 0000000..5de9a16 --- /dev/null +++ b/spikes/AvaloniaPdfSpike/App.axaml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/spikes/AvaloniaPdfSpike/App.axaml.cs b/spikes/AvaloniaPdfSpike/App.axaml.cs new file mode 100644 index 0000000..6deeabb --- /dev/null +++ b/spikes/AvaloniaPdfSpike/App.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace AvaloniaPdfSpike; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/spikes/AvaloniaPdfSpike/AvaloniaPdfSpike.csproj b/spikes/AvaloniaPdfSpike/AvaloniaPdfSpike.csproj new file mode 100644 index 0000000..b3b95c9 --- /dev/null +++ b/spikes/AvaloniaPdfSpike/AvaloniaPdfSpike.csproj @@ -0,0 +1,27 @@ + + + WinExe + net8.0 + enable + app.manifest + true + + + + + + + + + + None + All + + + + + + + diff --git a/spikes/AvaloniaPdfSpike/MainWindow.axaml b/spikes/AvaloniaPdfSpike/MainWindow.axaml new file mode 100644 index 0000000..4d8675f --- /dev/null +++ b/spikes/AvaloniaPdfSpike/MainWindow.axaml @@ -0,0 +1,64 @@ + + + + + + + + + +