How the codebase is organised and where new code goes. Deliberately pragmatic: a thin core, feature-sliced UI, and three projects — no Clean-Architecture ceremony, no speculative scaffolding. (A 2026 modernisation audit rejected a heavier 4-project Clean-Architecture design as over-engineering for an app of this size; we keep it light and grow by feature.)
Native cross-platform desktop markdown / code viewer. Avalonia 11.3 (Skia, no WebView), .NET 9, MVVM (CommunityToolkit.Mvvm), DI (Microsoft.Extensions.DependencyInjection), Central Package Management.
┌─────────────────────────────┐
│ Tittle (UI, Avalonia) │ Features/ · Platform/ · Shared/ · Themes/
│ depends on ▼ │
├─────────────────────────────┤
│ Tittle.Core (net9.0) │ pure logic + Abstractions (ports). NO Avalonia.
└─────────────────────────────┘
Tittle.Tests mirrors both (Headless UI + unit).
| Project | Role | Rules |
|---|---|---|
| Tittle.Core | Pure logic + ports (interfaces). Abstractions/ (IFileReader, IFileDialogService, IThemeService+ThemeMode, ISettingsStore, IAppSettingsService, IRecentFilesStore), Services/ (RecentFilesList, FileReader, AppSettingsService), Text/ (TextMetrics, MarkdownFile/Link/Preprocessor/Outline, TextEncodingDetector, BinaryContent, LineEndings), Documents/ (FileLoadResult, FileLimits), Settings/ (AppSettings, WindowPlacement, SessionState, EditorSettings, WindowPlacementValidator), Diagnostics/ (CrashLog). One BCL dep (System.Text.Encoding.CodePages) for Windows-1251. |
No Avalonia, no UI. No DDD layers — keep it flat and small. |
| Tittle (UI, WinExe) | Avalonia app. Features/ (Shell, Welcome, Viewer, …), Platform/ (Avalonia/IO port impls), Shared/ (cross-feature VM base, converters, EditorOptions — shared editor font/wrap/line-numbers), Themes/, App. AssemblyName=Tittle. |
Talks to the outside world only through Core ports. |
| Tittle.Tests | xUnit + Avalonia.Headless. Mirrors structure: Core/, Features/, Platform/. |
Pure logic → [Fact]; UI/resources → [AvaloniaFact] + TestAppBuilder. |
Arrows point inward only: UI → Core; Core depends on nothing (no Avalonia). The UI reaches files,
dialogs, theme, storage etc. only via Core/Abstractions ports, implemented in Platform/.
Never using Avalonia inside Core.
- Create
Features/<Name>/with<Name>View.axaml(.cs)+<Name>ViewModel.cs(namespaceTittle.Features.<Name>; VM derivesViewModelBasefromShared/). - Need the outside world (files, shell, clipboard, a renderer)? Add a port interface to
Core/Abstractionsand its implementation inPlatform/(or pure logic inCore). Create the port only when its real consumer exists (see §7). - Register in DI (
App.axaml.cs ConfigureServices, or a smallAdd<Name>()extension). - Add a mirrored test under
Tests/Features/<Name>/(orCore/for pure logic).
One feature = one commit carrying View+VM + any port + implementation + test together.
Source of features: E:\Scripts\Markdown Viewer. Implement incrementally per roadmap (M3+).
| Original domain | Lands in |
|---|---|
| Markdown rendering (GFM, tables, footnotes, admonitions) | DONE (M3) — a View control (Features/Viewer/DocumentView hosts Markdown.Avalonia's MarkdownScrollViewer) + pure Core/Text/MarkdownPreprocessor (GitHub alerts → ::: containers, task lists → glyphs, footnotes) + Features/Viewer/AdmonitionBlockHandler (IContainerBlockHandler). No IMarkdownRenderer port — it's a control, not a swappable service (YAGNI, §7). |
| Code highlighting / decoration | Features/Viewer (EditorBehavior / TextMate) |
| Diagrams (Mermaid/PlantUML), Math (KaTeX) | port IDiagramRenderer + Features/Viewer |
| TOC / outline | DONE (M4) — pure Core/Text/MarkdownOutline (heading parse) + Features/Viewer/OutlinePanel sidebar; navigation scrolls the source editor by line, or the preview in place by walking the visual tree (DocumentView, no port). In-document find is DONE (M9) — pure Core/Text/TextSearch + a Ctrl+F find bar in Features/Viewer/DocumentView (source-only; an IBackgroundRenderer highlights matches). |
| Sync-scroll | behaviour in Features/Viewer (no port) |
| Export PDF/HTML | port IExporter + Platform/ |
| Theme presets | Themes/ (+ optional IThemePresetProvider) |
| Settings / window state | DONE (M6) — typed Core/Settings/AppSettings held by IAppSettingsService (AppSettingsService), persisted atomically via ISettingsStore as one settings.json. Theme + window placement + session restore. |
| Tabs / session restore | DONE (M6) — session (open files + active tab) is a field in AppSettings, saved on window close and restored at startup. No ISessionStore port — the holder is the seam (YAGNI, §7), mirroring how M3 skipped IMarkdownRenderer. |
| Live-reload | port IDocumentWatcher (FileSystemWatcher impl in Platform/) |
| Bookmarks | port IBookmarkStore |
Except where marked DONE, these ports/models do not exist yet — add each with its feature.
Note how markdown rendering landed without the speculatively-mapped IMarkdownRenderer port:
the renderer turned out to be a control, so the only Core addition was pure preprocessing logic.
Treat the remaining rows as direction, not committed contracts.
- Namespace = folder.
Tittle.Features.<Name>,Tittle.Platform,Tittle.Shared,Tittle.Core.*. (App/Programstay at rootTittle.) Keeps IDE0130 happy. - avares:// uses the AssemblyName (
Tittle), not folder/namespace —avares://Tittle/Themes/.... Don't rename the assembly without checking theme loading (ThemeServiceTests.ColorTokensis the canary). - MVVM:
[ObservableProperty]/[RelayCommand]source generators (VMs arepartial). Compiled bindings on by default → every View/DataTemplate needsx:DataType. - DI: everything resolved from the
ServiceProviderinApp.axaml.cs. Single window resolved directly (no ViewLocator yet — see §8). - Theming: chrome colours are
{DynamicResource ...Brush}tokens inThemes/Colors/{Light,Dark}.axaml; never hard-code hex in views. - CPM: package versions in
Directory.Packages.props; shared MSBuild props inDirectory.Build.props.
Do not create ports, domain models, or abstractions ahead of a real consumer. Introduce an abstraction when there is an actual implementation and caller in the same commit (Rule of Three for duplication). "Readiness to fill in" comes from this document + feature folders, not from speculative empty code.
Not needed yet — one window, resolved from DI. Introduce a DI-aware ViewLocator (IDataTemplate,
VM→View) and an INavigationService port only when the first navigable feature appears (e.g. a Settings
dialog or Search overlay).
dotnet build Tittle.sln -c Debug # build
dotnet test Tittle.sln # unit + Headless UI (baseline: 28)
dotnet format Tittle.sln # CI verifies formatting
dotnet run --project src/Tittle # run (or: Tittle <file>)