Я хотел заметочник, где заметки — обычные .md‑файлы в обычной папке, есть [[ссылки]] с обратными ссылками и поиск, а текст сразу выглядит оформленным, без отдельной панели предпросмотра. И чтобы он запускался быстро, не тащил в память целый браузер и не требовал аккаунта. Готовые варианты почти все построены на WebView или Electron. Я написал свой: Leaflet, C# и Avalonia, без единой веб‑страницы внутри. Расскажу, как он устроен и где пришлось повозиться.

Что хотелось получить
Заметки — файлы
.mdв любой папке; программа ничего не прячет в базу.[[Имя]],[[Имя#Раздел]],[[Имя|подпись]], обратные ссылки, теги#проект/сайт, поиск по всем заметкам.Живой просмотр прямо в редакторе: строка с курсором показывает исходный Markdown, остальные — оформленный текст.
Экспорт в HTML и PDF.
Папка с exe, которая работает на чистой Windows 10/11 без установки.
Как это устроено
Хранилище. Vault читает все .md в выбранной папке (скрытые .папки пропускает), строит индекс ссылок и следит за папкой через слежение за файлами. Запись идёт через временный файл, чтобы сбой посреди записи не оставил половину заметки. При переименовании или переносе заметки Vault.Move правит [[ссылки]] во всех остальных заметках, сохраняя #раздел и |подпись; объект заметки при этом остаётся тем же, поэтому открытые вкладки и история не рвутся.
Редактор. Основа — AvaloniaEdit, порт AvalonEdit. Живой просмотр сделан не заменой документа, а оформлением строк поверх исходного текста: генераторы элементов и трансформеры скрывают разметку и красят куски на всех строках, кроме той, где стоит каретка. Текст файла при этом всегда остаётся исходным Markdown, и отмена, поиск и выделение работают как в обычном редакторе.
Из этого получаются остальные возможности:
таблица — одна строка редактора превращается в один элемент с ячейками, сетку рисует отдельный рендерер; строка с курсором показывает исходник, а её
|выравниваются по линиям сетки (второй проход поVisualLinesChanged);список — маркер с отступом становится одним элементом, уровень считается по отступу, нумерация остаётся текстом;
- [ ]и- [x]показываются флажками, щелчок меняет один знак и создаёт свой шаг отмены;картинка
— после строки ставится ряд с изображением, которое декодируется в фоне сразу в нужную ширину;встраивание
![[Имя]]— карточка с содержимым другой заметки, глубина вложенности не больше четырёх, круговое встраивание обнаруживается.
Почему Avalonia, а не WebView
WebView или Electron дали бы готовый Markdown‑рендер за вечер. Но вместе с ним — второй процесс с тяжёлой памятью, заметная задержка старта и вопросы по обновлению среды. Мне нужен был маленький «родной» процесс. Avalonia 12 на.NET 8 рисует через Skia, работает на Windows 10/11 и позволяет ограничиться одним процессом. Побочный плюс: PDF тоже собирается без браузера, через Skia (ниже).
Цена: интерфейс пришлось писать кодом. Установленный на машине SDK 8.0.403 старше Roslyn 4.14, из‑за чего XAML не компилировался (ошибка CS9057), поэтому весь UI написан кодом на C#.
Что получилось по замерам на заметке в 5 013 строк (216 тыс. символов), окно 1200×780 с панелями, сборка ReadyToRun:
до первого кадра 0,66–0,89 с;
рабочий набор после загрузки около 133 МБ;
обработка символа (ввод + раскладка) в среднем 0,4 мс;
Enter в среднем 3 мс.
Это мои замеры на одной машине, для ориентира, не бенчмарк для сравнения.
Подводные камни
Окно без белой полосы. Своя шапка с вкладками и кнопками окна нужна, чтобы не терять место. Но с полным режимом декораций Avalonia 12 рисует собственный заголовок поверх вкладок, а без расширения клиентской области на Windows 10 сверху видна белая полоса в 7 пикселей. Рабочая комбинация такая:
WindowDecorations = WindowDecorations.BorderOnly; ExtendClientAreaToDecorationsHint = true;
Свои кнопки окна — отдельно. Я проверил перетаскивание, двойной щелчок, края и углы, разворот ровно по рабочей области, сворачивание и закрытие.
Editor.Focus() ничего не делает. TextEditor не принимает фокус сам: метод возвращает false, а фокус остаётся нигде. Фокус в редактор ставится через Editor.TextArea.Focus(). Похожая мелочь: если скрыть панель, в которой стоит фокус, сначала нужно перевести фокус в редактор и только потом делать IsVisible = false.
Свёрнутая строка таблицы. Разделитель |---| скрывается, чтобы строка не оказывалась ниже шрифта (CollapseLines). Но свёрнутую строку обязан покрыть элемент предыдущей строки (шапка тянется до конца разделителя), иначе AvaloniaEdit падает. Узнаётся только экспериментом.
Каретка рядом с картинкой. Без метки нулевой ширины в конце строки каретка рисуется на ряду картинки во всю высоту. Пришлось ставить такую метку и отдельно обрабатывать стрелки: вниз — на следующую строку, вверх или щелчок — в конец текста, чтобы каретка не стояла в ряду картинки.
Скорость при вводе. Карта блоков (текст, код, строки таблицы) пересобирается только при вводе или удалении `, |, перевода строки, а - и : — если в строке есть |. Проход по doc.Text как по строке оказался в разы дешевле обхода doc.Lines через GetCharAt: на 5 000 строк разница была около 1,2 мс на каждый Enter.
PDF без браузера. Экспорт делает SKDocument.CreatePdf из SkiaSharp, он уже входит в Avalonia, поэтому WebView не нужен. Вёрстка своя: A4, цвета выбранной темы, перенос по словам, номера страниц, ссылки аннотациями. Но сборка SkiaSharp идёт без урезателя шрифтов и встраивает их целиком: одна страница весила 1,6 МБ. Пришлось писать свой FontSubset: номера глифов остаются прежними, ненужные контуры становятся пустыми, делается свой cmap и убираются таблицы GSUB и GPOS. Страница стала около 130 КБ. Шрифты CFF (OTTO) не урезаются.
Данные важнее красоты. Программа лежит в папке с моими настоящими заметками, поэтому над надёжностью пришлось поработать:
файл изменили снаружи, пока в программе есть несохранённые правки: вкладка получает состояние «конфликт» и полосу с вариантами («Оставить мою», «Взять с диска», «Сохранить мою как копию», «Сравнить»); перед записью дата файла сверяется с той, что программа видела, чтобы не затереть чужую правку, если слежение не успело сработать;
черновики пишутся в фоне через 1,5 с после правки и удаляются при сохранении; после настоящего сбоя программа предложит их восстановить;
снимки версий с разницей по строкам (алгоритм Майерса), с пределом 50 на заметку и 100 МБ в сумме, возврат к версии делается правкой, так что Ctrl+Z его тоже отменяет;
заметку переименовали в проводнике: если пропала одна и в той же пачке появилась другая с тем же текстом, считаем это переименованием, и вкладка идёт за файлом.
Для проверки сбоя самопроверка запускает второй экземпляр программы, правит без автосохранения, ждёт появления черновика и убивает процесс через Process.Kill(). Потом родитель проверяет файл и восстановление.
Один экземпляр. Мьютекс с именем Local\Leaflet.Mutex.<хэш папки данных> плюс именованный канал: второй запуск передаёт первому пути и ждёт «ok», а первое окно выходит вперёд. Хэш папки данных в имени нужен, чтобы портативная копия, обычная и тесты не мешали друг другу.
Положение окна на нескольких мониторах. Размеры хранятся в логических единицах, положение в пикселях, плюс масштаб монитора. При запуске список мониторов (EnumDisplayMonitors и GetDpiForMonitor) помогает выбрать тот, с которым окно пересекается больше всего, сдвинуть его внутрь рабочей области и убедиться, что полоса заголовка не меньше 100 пикселей. Отдельно — окно, закрытое в развёрнутом виде: тогда в момент закрытия Avalonia отдаёт границы монитора, а не обычные.
Масштаб Windows. Масштаб на моей машине не менялся, поэтому самопроверка шлёт окну настоящий WM_DPICHANGED и проверяет: Avalonia пересчитала масштаб, логический размер окна сохранился, границы панелей остались на целых пикселях. Живой перенос между мониторами с разным масштабом на настоящем железе я не проверял — это честное ограничение.
Сборка и поставка
Основной zip — self‑contained: работает на чистой Windows без.NET. Размер папки 115 МБ, zip около 50 МБ, против 44 и 18 МБ у варианта, которому нужен установленный.NET 8 Runtime. Запуск и память при этом одинаковые, так что разницы по скорости нет, выбираем удобство.
dotnet publish src/Leaflet -c Release -r win-x64 --self-contained true -p:PublishReadyToRun=true
Single‑file и обрезку (trim) я не делал: Avalonia и AvaloniaEdit используют рефлексию, а выигрыш в размере не стоит риска. Из поставки убраны *.pdb: у SkiaSharp и HarfBuzz они весят около 100 МБ. В конце build.ps1 распаковывает zip в чистую временную папку, запускает exe с отдельной папкой данных и ждёт появления окна.
Программа не подписана цифровой подписью, поэтому SmartScreen может показать синее окно: «Подробнее» → «Выполнить в любом случае».
Что получилось
Три темы («Графит», «Терминал» и «Светлая»), интерфейс на русском и английском, настраиваемые горячие клавиши, палитра команд, встроенная справка.


Проект проверяется собственной самопроверкой на 384 проверки. Она запускает настоящее окно и шлёт настоящие нажатия клавиш и мыши (Ctrl+F, набор [[, перетаскивание файлов, Ctrl+V с картинкой в буфере), поэтому на несколько минут берёт клавиатуру и мышь на себя и запускается только на копии тестовой папки.
Чего нет: плагинов, синхронизации, мобильных версий. Это заметочник «папка с файлами», остальное можно сделать поверх своими средствами (git, облачная папка).
Ссылки
Страница проекта и исходный код (MIT): https://github.com/OlegChe77/leaflet
Готовый zip (около 50 МБ, Windows 10/11, ничего ставить не нужно): https://github.com/OlegChe77/leaflet/releases/latest/download/Leaflet.zip
Проект бесплатный. Предложения и найденные ошибки — в комментариях или в Issues на GitHub.
Комментарии (15)

NeoCode2
30.10.2026 20:43Думаю такую же штуку написать на Qt, там есть готовый QTextEdit поддерживающий форматированный текст и при этом не являющийся браузером. Сейчас для аналогичных целей использую Qt-шный браузерный виджет в режиме editable, а заметки в html, но все же прихожу к выводу что md лучше.

Siemargl
30.10.2026 20:43Скоро 10 лет как статье (а этой идее более 15)

NeoCode2
30.10.2026 20:43Там прямо из скриншотов видно, что это не то: вместо двух панелей зачем-то три (как в почтовике), и самое главное - список выбора шрифта, т.е. там форматирование вместо разметки. В своей программе я использую очень ограниченное подмножество html, но к сожалению сам редактор невозможно ограничить - и при редактировании он внутри себя нередко создает спаны и дивы с явным форматированием. А современные браузерные виджеты еще и не дают прямого доступа к DOM, вынуждая делать всё через дурацкий асинхронный javascript.

Siemargl
30.10.2026 20:43там форматирование вместо разметки
Не понял разницу. На выходе html, на входе QTextEdit (вроде. исходники доступны)
А третья панель там - иерархия заметок. Например - Работа - Проекты - ПроектИКС, и к нему список заметок. Удобно, и есть дополнительно тегирование

diamon
30.10.2026 20:43обсидиан же есть, зачем изобретать велосипед? впрочем в эпоху аи каждый делает удобный инструмент под себя...

beskaravaev
30.10.2026 20:43Звучит очень интересно, но с названием есть беда. При поиске будете конфликтовать с другой библиотекой, которая уже очень популярна. Да и нехорошо получается, что используете уже занятое название
Leaflet — библиотека с открытым исходным кодом, написанная на JavaScript, предназначенная для отображения карт на веб-сайтах.

Mike-M
30.10.2026 20:43запуск за секунду
Для меня это долго. Привык к С++ программам, первый кадр которых появляется сразу после запуска .exe.

Siemargl
30.10.2026 20:43Вам нужно апгрейдить свой компьютер /s
Авалония. Штош поделать

MonkAlex
30.10.2026 20:43Авалония в нативе работает хорошо. Профилировать надо и смотреть, куда секунда уходит.

Siemargl
30.10.2026 20:43Я и так скажу - на загрузку более 100мб нетовских длл с диска.
Естественно, если постараться и сделать под АОТ, время загрузки будет пропорционально размеру меньше

MonkAlex
30.10.2026 20:43Да, это и имел в виду. Где хочется (и можется) быстрый запуск, там АОТ или хотя бы р2р очень даже приятны. И у авалонии оно работает, насколько я помню, т.е. фреймворк не мешает.

psyberia
Отличная идея! Давно хотел найти заметочник, который не городит никаких баз, а просто читает файлы .md, которые лежат на диске. Как это делает с картинками любой просмотрщик изображений, например XnView. Он может индексировать картинки, редактировать их, но основная его функция - просмотр. Внешним редактором изображений пользуешься или внутренним (самим XnView) - без разницы, хранилищем в любом случае является диск (папки, в которых лежат те или иные картинки). Так что даже если программа перестанет работать, никакой угрозы пользователю это не несёт. Удалил программу, поставил другую. Все твои файлы как лежали на диске, так и лежат.