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

Что хотелось получить

  • Заметки — файлы .md в любой папке; программа ничего не прячет в базу.

  • [[Имя]], [[Имя#Раздел]], [[Имя|подпись]], обратные ссылки, теги #проект/сайт, поиск по всем заметкам.

  • Живой просмотр прямо в редакторе: строка с курсором показывает исходный Markdown, остальные — оформленный текст.

  • Экспорт в HTML и PDF.

  • Папка с exe, которая работает на чистой Windows 10/11 без установки.

Как это устроено

Хранилище. Vault читает все .md в выбранной папке (скрытые .папки пропускает), строит индекс ссылок и следит за папкой через слежение за файлами. Запись идёт через временный файл, чтобы сбой посреди записи не оставил половину заметки. При переименовании или переносе заметки Vault.Move правит [[ссылки]] во всех остальных заметках, сохраняя #раздел и |подпись; объект заметки при этом остаётся тем же, поэтому открытые вкладки и история не рвутся.

Редактор. Основа — AvaloniaEdit, порт AvalonEdit. Живой просмотр сделан не заменой документа, а оформлением строк поверх исходного текста: генераторы элементов и трансформеры скрывают разметку и красят куски на всех строках, кроме той, где стоит каретка. Текст файла при этом всегда остаётся исходным Markdown, и отмена, поиск и выделение работают как в обычном редакторе.

Из этого получаются остальные возможности:

  • таблица — одна строка редактора превращается в один элемент с ячейками, сетку рисует отдельный рендерер; строка с курсором показывает исходник, а её | выравниваются по линиям сетки (второй проход по VisualLinesChanged);

  • список — маркер с отступом становится одним элементом, уровень считается по отступу, нумерация остаётся текстом; - [ ] и - [x] показываются флажками, щелчок меняет один знак и создаёт свой шаг отмены;

  • картинка ![](assets/x.png) — после строки ставится ряд с изображением, которое декодируется в фоне сразу в нужную ширину;

  • встраивание ![[Имя]] — карточка с содержимым другой заметки, глубина вложенности не больше четырёх, круговое встраивание обнаруживается.

Почему 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, облачная папка).

Ссылки

Проект бесплатный. Предложения и найденные ошибки — в комментариях или в Issues на GitHub.

Комментарии (15)


  1. psyberia
    30.10.2026 20:43

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


  1. NeoCode2
    30.10.2026 20:43

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


    1. Siemargl
      30.10.2026 20:43

      Скоро 10 лет как статье (а этой идее более 15)

      Самая нужная программа на свете


      1. NeoCode2
        30.10.2026 20:43

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


        1. Siemargl
          30.10.2026 20:43

          там форматирование вместо разметки

          Не понял разницу. На выходе html, на входе QTextEdit (вроде. исходники доступны)

          А третья панель там - иерархия заметок. Например - Работа - Проекты - ПроектИКС, и к нему список заметок. Удобно, и есть дополнительно тегирование


  1. diamon
    30.10.2026 20:43

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


    1. keelsee
      30.10.2026 20:43

      На Rust уже был - https://habr.com/ru/articles/1090122/, теперь на C#


  1. solus_factor
    30.10.2026 20:43

    Классная штука!


  1. beskaravaev
    30.10.2026 20:43

    Звучит очень интересно, но с названием есть беда. При поиске будете конфликтовать с другой библиотекой, которая уже очень популярна. Да и нехорошо получается, что используете уже занятое название

    Leaflet — библиотека с открытым исходным кодом, написанная на JavaScript, предназначенная для отображения карт на веб-сайтах.


  1. Mike-M
    30.10.2026 20:43

    запуск за секунду

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


    1. Siemargl
      30.10.2026 20:43

      Вам нужно апгрейдить свой компьютер /s

      Авалония. Штош поделать


      1. MonkAlex
        30.10.2026 20:43

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


        1. Siemargl
          30.10.2026 20:43

          Я и так скажу - на загрузку более 100мб нетовских длл с диска.

          Естественно, если постараться и сделать под АОТ, время загрузки будет пропорционально размеру меньше


          1. MonkAlex
            30.10.2026 20:43

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


  1. VictorNS
    30.10.2026 20:43

    Из статьи не понял, а почему dotNET 8 а не 10?