Нативное приложение для macOS, которое мгновенно ищет по тексту заметок и документов в одном выбранном каталоге. Вызывается глобальным сочетанием клавиш, показывает список файлов и контекст найденного места с подсветкой, а Enter открывает файл в привычной программе.
- NoteSearch
NoteSearch нужен тем, кто хранит знания в обычных файлах: Markdown-заметки, конфигурации, скрипты, документация. Типичный сценарий:
⌘⌥Space → ввести «домашний сервер» → увидеть список файлов → выбрать «Настройка сервера.md»
→ увидеть фрагмент с подсветкой → нажать Enter → файл открылся
Главный приоритет: хоткей → запрос → файл → контекст → открыть.
Принципы проекта:
- Один каталог. Индексируется только то, что пользователь выбрал явно. Нельзя выбрать
/, домашнюю папку и~/Library. - Всё локально. Нет сети, нет учётных записей, нет телеметрии. Индекс лежит в
~/Library/Application Support/NoteSearch/. - Быстро. Поиск через SQLite FTS5, выполняется в фоне и не блокирует интерфейс. Изменения файлов подхватываются на лету.
- Только чтение. Приложение ничего не редактирует и не изменяет в ваших файлах.
Поиск
- Полнотекстовый поиск по содержимому, имени и пути файла.
- Несколько слов означают «документ содержит все слова».
- Поиск по началу слова:
servнаходит «server». - Точная фраза в кавычках:
"домашний сервер". - Не зависит от регистра. Русский, английский и смешанный текст, дефисы и подчёркивания.
ёиесчитаются одной буквой, айииразными.- Ранжирование по релевантности: учитываются имя файла, целые слова, свежесть.
Результаты и предпросмотр
- Список с именем, относительным путём, типом, датой изменения и числом совпадений.
- Фрагменты вокруг совпадений (180 символов до и 300 после), несколько фрагментов с кнопкой «Показать ещё».
- Подсветка найденного, номер строки фрагмента.
- Markdown: подсветка заголовков, списков, цитат.
- Блоки кода (с указанием языка, например
bash,py,json) отображаются отдельной карточкой с подсветкой синтаксиса и кнопкой копирования. Файлы с кодом (.py,.js,.json,.yaml,.sqlи др.) показываются как код целиком.
Индекс
- Первичная индексация с прогрессом (
Индексирование: 124 / 850 файлов). - Сверка с диском при каждом запуске: подхватываются изменения, сделанные пока приложение было закрыто.
- Отслеживание файловой системы (FSEvents): создание, изменение, удаление и перемещение файлов и папок обновляют только затронутые документы.
- Ошибка на одном файле не останавливает индексацию, количество пропущенных файлов показывается в статусе.
Интерфейс и система
- Панель в стиле Spotlight, появляется по центру экрана поверх других окон, в том числе над полноэкранными приложениями.
- Значок в строке меню, возможность скрыть значок в Dock.
- Настраиваемое глобальное сочетание клавиш.
- Тема: системная, светлая, тёмная.
- Запуск вместе с macOS.
- Работа с клавиатуры.
- macOS 14 или новее, в первую очередь Apple Silicon (разрабатывалось на MacBook Air M1).
- Xcode 15 или новее (Swift 5.9+) для сборки.
- Python 3 нужен только для вспомогательных скриптов.
git clone <адрес-репозитория> NoteSearch
cd NoteSearch
xcodebuild \
-project NoteSearch.xcodeproj \
-scheme NoteSearch \
-configuration Debug \
-destination 'platform=macOS,arch=arm64' \
build
open ~/Library/Developer/Xcode/DerivedData/NoteSearch-*/Build/Products/Debug/NoteSearch.appЛибо откройте NoteSearch.xcodeproj в Xcode и нажмите ⌘R. Открывать нужно именно файл NoteSearch.xcodeproj, а не вложенную папку.
- По умолчанию индексируется
~/Documents/Notes. Если папки нет, она создаётся. - Чтобы выбрать другой каталог: значок лупы в строке меню → «Настройки…» → «Каталог» → «Изменить…». Приложение предупредит, что старый индекс будет заменён.
- macOS может запросить доступ к папке «Документы». Разрешите, иначе читать файлы не получится.
- Дождитесь статуса «Готов к поиску» и вводите запрос.
По умолчанию задано ⌘⌥Space, но это сочетание в macOS по умолчанию занято системой («Показать окно поиска Finder»). Чтобы использовать его:
- System Settings → Keyboard → Keyboard Shortcuts… → Spotlight.
- Снимите галочку «Показать окно поиска Finder».
- Перезапустите NoteSearch.
Либо запишите другое сочетание: «Настройки → Окно → Глобальное сочетание», нажмите кнопку и введите новое (например, ⌃⌥⌘Space). Нужен хотя бы ⌘ или ⌃, Esc отменяет запись.
| Действие | Сочетание |
|---|---|
| Показать или скрыть панель | глобальное сочетание (по умолчанию ⌘⌥Space) |
| Фокус на поле поиска | ⌘K |
| Обернуть запрос в кавычки (точная фраза) / снять кавычки | ⌘' |
| Следующий результат | ↓ |
| Предыдущий результат | ↑ |
| Открыть файл | Enter |
| Показать файл в Finder | ⌘Enter |
| Очистить запрос | Esc или ⌘⌫ |
| Скрыть панель | Esc при пустом запросе |
После открытия файла панель скрывается. Она также скрывается при клике вне панели (отключается в настройках).
| Запрос | Что найдёт |
|---|---|
docker compose |
документы, где есть оба слова (каждое по началу слова) |
dock comp |
то же самое: слова ищутся по началу |
"домашний сервер" |
точная фраза целыми словами |
uv-proekt |
слово с дефисом, ищется как единое выражение |
елка |
«ёлка» и «елка» |
Запрос короче двух символов не выполняется, приложение подскажет об этом.
Значок лупы открывает меню: «Открыть поиск», «Настройки…», «Переиндексировать», «Выйти из NoteSearch». Если скрыть значок в Dock, закрыть приложение можно только через это меню.
Запрос разбирается в список терминов:
- слово без кавычек: термин-префикс, превращается в FTS-условие
"слово"*; - текст в кавычках: термин-фраза, превращается в
"точная фраза"без звёздочки; - термины без букв и цифр (например,
---) отбрасываются; - термины соединяются через
AND.
Пример: docker "домашний сервер" превращается в "docker"* AND "домашний сервер".
Текст файлов, имена и пути при индексации, а также термины запроса приводятся к одному виду:
- Юникод-нормализация NFC (разложенные буквы вроде
и+ знак краткости собираются в одну). - Замена
ёнае(иЁнаЕ). - Регистр убирает токенайзер FTS5.
Буква й остаётся отдельной буквой: «мой» не находит «мои». Это поведение закреплено тестами.
Подсветка в превью устроена по тем же правилам: регистр не важен, е и ё равнозначны, й и и различаются, совпадение должно начинаться с начала слова.
SQLite отдаёт до 200 кандидатов, отсортированных по bm25 с весами колонок: имя файла ×8, путь ×0,5, содержимое ×1. Затем приложение пересчитывает оценку и оставляет 100 лучших:
score = релевантность (bm25, нормирована к 0…1)
+ 2,0 × доля слов запроса, найденных целым словом
+ 1,5 × доля слов запроса, найденных в имени файла
+ 0,3 × свежесть (линейно падает за 90 дней)
При равенстве файлы сортируются по имени. Бонусы за целое слово и за имя файла перевешивают разброс bm25, поэтому порядок предсказуем даже на маленьких каталогах. Пример: для запроса server выше окажется server.md (имя и целое слово), затем файл с отдельным словом «server» в тексте, затем файл со словом «serverless».
Запрос запускается после паузы ввода (300 мс). Выполняется в отдельной задаче: предыдущий поиск отменяется, как только вы продолжаете печатать, а устаревшие результаты отбрасываются. Старый список остаётся на экране, пока считается новый, поэтому интерфейс не мигает.
Для выбранного файла приложение читает текст и строит фрагменты:
- вокруг каждого совпадения берётся окно из 180 символов до и 300 после;
- пересекающиеся окна склеиваются в один фрагмент;
- по умолчанию показываются 5 фрагментов, остальные открывает кнопка «Показать ещё»; всего строится до 50;
- у каждого фрагмента есть номер строки, с которой он начинается;
- для Markdown подсвечиваются заголовки, маркеры списков и цитаты;
- огороженные блоки кода (три обратных апострофа или три тильды, язык после ограды) рисуются отдельной карточкой. Если фрагмент начался в середине блока, приложение знает его язык, потому что состояние блока считается по всему документу;
- подсветка синтаксиса: shell/bash/zsh, Python, JavaScript/TypeScript, JSON, YAML, Swift, HTML/XML, CSS, SQL и C-подобные языки (Go, Rust, Java, Kotlin, C, C++, C#, PHP). У неизвестного языка блок остаётся блоком, но без раскраски.
База SQLite находится в ~/Library/Application Support/NoteSearch/index.db (режим WAL).
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
path TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
extension TEXT NOT NULL,
modified_at REAL NOT NULL,
file_size INTEGER NOT NULL,
content_hash TEXT NOT NULL -- SHA-256 содержимого
);
CREATE VIRTUAL TABLE documents_fts USING fts5(
file_name, path, content,
modified_at UNINDEXED,
tokenize = 'unicode61 remove_diacritics 2',
prefix = '2 3 4' -- индексы префиксов для быстрого поиска по началу слова
);Строка в documents_fts имеет rowid, равный documents.id: так таблицы связываются, а обновление и удаление выполняются быстро. В FTS попадает нормализованный текст (см. выше), поэтому размер индекса сопоставим с размером индексируемых текстов.
Версия схемы хранится в PRAGMA user_version. При смене версии индекс пересоздаётся автоматически.
- Рекурсивный обход выбранного каталога (скрытые файлы пропускаются).
- Проверка расширения (без учёта регистра) и исключений.
- Чтение файла как UTF-8; файлы больше 10 МБ и файлы, которые не читаются, пропускаются.
- Запись документа и его текста в индекс в одной транзакции.
- Прогресс в статусе; пропущенные файлы считаются и выводятся в статусе (
Готов к поиску · пропущено: 1), причины пишутся в журнал.
Индекс может отстать, если файлы менялись, пока приложение было закрыто. Поэтому при каждом запуске:
- Запускается отслеживание файловой системы (чтобы не потерять события).
- Состояние диска сравнивается с индексом по дате изменения и размеру.
- Новые файлы добавляются, изменённые перечитываются, пропавшие (а также ставшие исключёнными) удаляются.
Если каталога нет на месте (например, отключён диск), индекс не стирается.
Используется FSEvents с событиями уровня файлов и задержкой 0,5 с. Для пачки событий приложение:
- файл создан или изменён: перечитывает только его;
- файл удалён или перемещён: удаляет запись;
- папка создана или перемещена: сканирует только её;
- папка удалена: удаляет все документы внутри;
- исключённые, скрытые и внешние пути игнорирует.
Открытый список результатов обновляется сам, выбранный файл остаётся выбранным.
Все пути приводятся к каноническому виду (realpath), чтобы каталоги за символическими ссылками (например, /tmp или /var) не создавали дубликатов.
- Индексируется только явно выбранный каталог. Выбрать
/, домашнюю папку или~/Libraryнельзя. - При смене каталога приложение предупреждает о замене индекса, удаляет старый и создаёт новый.
- Файлы никогда не изменяются.
- Приложение не использует сеть. Сборка использует Hardened Runtime, App Sandbox не включён (иначе потребовался бы доступ через security-scoped bookmarks).
Открываются из строки меню («Настройки…») или кнопкой ⚙︎ в панели.
| Вкладка | Что настраивается |
|---|---|
| Каталог | индексируемый каталог (диалог выбора и предупреждение о замене индекса), открыть в Finder, запуск вместе с macOS |
| Окно | тема (системная, светлая, тёмная), глобальное сочетание, скрытие панели при потере фокуса, показ панели при запуске, значки в строке меню и в Dock (хотя бы один всегда включён) |
| Исключения | список исключаемых папок: по имени (node_modules) или по полному пути; добавление текстом или выбором папки, удаление, сброс |
| Типы файлов | список индексируемых расширений: добавление, удаление, сброс |
| Индекс | число документов, дата последней индексации, размер индекса, «Переиндексировать», «Очистить индекс» |
После изменения исключений или расширений появляется плашка «Настройки изменены» с кнопкой «Переиндексировать».
| Параметр | Значение |
|---|---|
| Каталог | ~/Documents/Notes |
| Расширения | md, markdown, txt, json, yaml, yml, py, js, ts, html, htm, css |
| Исключения | .git, .venv, venv, node_modules, __pycache__, .cache, .build, DerivedData, Images |
| Нельзя добавить как расширения | png, jpg, jpeg, gif, webp, heic, svg, mp4, mov, zip, tar, gz |
| Сочетание | ⌘⌥Space |
| Тема | системная |
Настройки хранятся в UserDefaults (идентификатор приложения ru.vincento.NoteSearch): rootPath, supportedExtensions, excludedEntries, lastIndexedAt, showMenuBarIcon, showDockIcon, hidePanelOnBlur, showPanelOnLaunch, hotkeyKeyCode, hotkeyModifiers, hotkeyLabel, appTheme.
NoteSearch/
├── Package.swift # пакет NoteSearchCore для запуска тестов (swift test)
├── NoteSearch.xcodeproj # проект приложения
├── NoteSearch/
│ ├── NoteSearchApp.swift # точка входа, панель, окно настроек, строка меню
│ ├── Models/
│ │ ├── AppState.swift # состояние интерфейса, запуск поиска и индексации
│ │ └── SearchModels.swift # SearchResult, SearchSnippet
│ ├── Services/
│ │ ├── IndexService.swift # SQLite/FTS5, индексация, сверка, FileWatcher
│ │ ├── SearchService.swift # разбор запроса, поиск, ранжирование, фрагменты, подсветка кода
│ │ └── GlobalHotkeyHandler.swift # глобальное сочетание, тема, ключи настроек
│ └── Views/
│ ├── ContentView.swift # три зоны: поиск, список, предпросмотр
│ ├── SearchBarView.swift # поле поиска, статус, клавиши
│ ├── ResultsListView.swift # список результатов
│ ├── PreviewView.swift # предпросмотр, блоки кода, палитра
│ └── SettingsView.swift # настройки, запись сочетания
└── Tests/NoteSearchCoreTests/ # автотесты
| Компонент | Ответственность |
|---|---|
QueryParser |
разбор строки запроса в термины, построение FTS-запроса |
TextNormalizer |
NFC и замена ё на е для индекса и запроса |
SearchService |
выполнение FTS-запроса, подсчёт совпадений, пересчёт оценки |
SnippetBuilder |
поиск совпадений, окна 180/300, блоки кода, номера строк |
SnippetSegmenter |
деление фрагмента на текст и блоки кода |
CodeHighlighter |
токенизатор для подсветки синтаксиса |
IndexService |
схема и миграции, индексация, сверка, применение событий файловой системы |
FileWatcher |
обёртка над FSEvents |
GlobalHotkeyHandler |
регистрация глобального сочетания (Carbon RegisterEventHotKey) |
PanelController |
панель NSPanel поверх окон (в том числе полноэкранных) |
AppState |
связывает интерфейс с поиском и индексом, управляет отменой запросов |
Поток данных:
ввод → debounce 300 мс → QueryParser → FTS5 (200 кандидатов) → пересчёт оценки (100 результатов)
→ список → выбор → чтение файла → SnippetBuilder → предпросмотр
FSEvents / запуск → IndexService (applyChanges / reconcile) → SQLite → обновление списка
Ядро (Models/SearchModels.swift, Services/SearchService.swift, Services/IndexService.swift) не зависит от интерфейса и подключается и в приложение, и в пакет тестов.
Тесты запускаются через Swift Package Manager, без ручной правки project.pbxproj:
swift test
# отдельные группы
swift test --filter QueryParserTests
swift test --filter IndexUpdateTestsТесты работают во временной папке и во временном наборе UserDefaults, поэтому ваш реальный индекс и настройки они не затрагивают.
| Набор | Что проверяет |
|---|---|
QueryParserTests |
слова, фразы, незакрытые кавычки, пунктуация, дефисы, FTS-запрос |
SnippetBuilderTests |
регистр и русский язык, начало слова, фразы, границы 180/300, склейка окон, номера строк, лимит |
IndexServiceTests |
расширения, исключения по имени и пути, битые и большие файлы, очистка, размер индекса |
SearchServiceTests |
все слова, регистр, фраза, префикс, дефис, относительный путь, число совпадений, ранжирование |
IndexUpdateTests |
сверка при запуске (добавление, изменение, удаление, перемещение, исключение), события файловой системы, реальный FileWatcher |
RussianFoldingTests |
ё и е, различие й и и, разложенный Юникод, подсветка по тем же правилам |
CodeSyntaxTests |
языки и псевдонимы, токены, ограды блоков кода, сегменты фрагментов |
Тест testWatcherReportsCreatedFile использует реальные события файловой системы и на медленном диске может быть нестабильным: swift test --skip testWatcherReportsCreatedFile.
Чек-лист основан на критериях приёмки из технического задания. Удобно использовать тестовый корпус: небольшой скрипт создаёт папку с файлами в разных регистрах, с кириллицей и скобками в путях, с каталогами node_modules, Images, .git, картинкой и заведомо битым файлом.
| Область | Проверка |
|---|---|
| Каталог | выбор через системный диалог, путь сохраняется после перезапуска, индексируется только выбранный каталог |
| Индексация | Markdown индексируется, картинки и исключённые папки нет, ошибка одного файла не останавливает процесс, виден прогресс и счётчик пропущенных |
| Поиск | запрос после паузы ввода, регистр не важен, русский язык, несколько слов, точная фраза, пустой и слишком короткий запрос |
| Список | имя, относительный путь, число совпадений, сортировка по релевантности, выбор мышью и клавиатурой |
| Предпросмотр | контекст 180/300, подсветка, несколько фрагментов и «Показать ещё», блоки кода, Enter открывает файл |
| Обновления | новый файл появляется, изменённый обновляется, удалённый исчезает, перемещение отражается |
| Запуск | изменения, сделанные при закрытом приложении, подхватываются при старте |
| Окно | сочетание вызывает панель из другого приложения (в том числе над полноэкранным), повторное нажатие скрывает |
| Тема | системная, светлая и тёмная корректно применяются ко всем окнам |
Сочетание ⌘⌥Space открывает поиск Finder. Оно занято системой. Отключите «Показать окно поиска Finder» в настройках клавиатуры или запишите другое сочетание в «Настройки → Окно».
Ничего не находится. Проверьте статус в верхней панели: должно быть «Готов к поиску». Убедитесь, что выбран нужный каталог и в нём есть файлы с поддерживаемыми расширениями. Если macOS ещё не дала доступ к папке «Документы», разрешите его в System Settings → Privacy & Security → Files and Folders.
Запуск при входе в систему не включается. Для надёжной работы приложение должно лежать в /Applications, а не в DerivedData.
В Dock значка нет, окно не открывается. Нажмите глобальное сочетание или значок лупы в строке меню. Если нужно вернуть значок в Dock: «Настройки → Окно → Значок в Dock».
Индекс испорчен или хочется начать с чистого листа.
pkill NoteSearch
rm -rf ~/Library/Application\ Support/NoteSearch
defaults delete ru.vincento.NoteSearch # сбрасывает и настройкиСборка не находит исходники. Проверьте, что NoteSearch.xcodeproj лежит рядом с папкой NoteSearch/, а исходники лежат в NoteSearch/NoteSearch/... ровно на один уровень вложенности.
- Поддерживаются только текстовые файлы в UTF-8. Другие кодировки (cp1251, UTF-16) пропускаются и учитываются в счётчике пропущенных.
- PDF, DOCX, изображения (OCR), архивы не индексируются.
- Поиск идёт по целым словам и по началу слова. Поиска по середине слова и учёта морфологии (падежи, чередования) пока нет.
- Системные сочетания клавиш (например,
⌘Space) приложение переопределить не может. - Markdown не рендерится: подсвечивается только его структура.
- Подсветка точной фразы ищет пробел буквально, поэтому фраза, разорванная переводом строки, в превью не подсвечивается, хотя документ найден.
- Проверялись Debug-сборки. Подпись Developer ID и нотаризация не настроены: скачанную сборку нужно разрешить вручную (System Settings → Privacy & Security → Open Anyway).
- Морфология русского языка (стемминг).
- Фильтры и операторы запроса: по расширению, папке, дате, исключение слова.
- Журнал пропущенных файлов с причинами в интерфейсе, чтение других кодировок.
- Обработчики PDF и DOCX.
- Release-сборка, подпись и нотаризация.
- Тестовый таргет в Xcode (запуск тестов по ⌘U), генерация проекта через XcodeGen.
- Поддержка файлов iCloud, которые ещё не скачаны на Mac.
- Открытие файла сразу на найденной строке.
Проект распространяется под лицензией MIT. Текст лицензии находится в файле LICENSE.
Проект разработан по техническому заданию NoteSearch_TZ.md.
История изменений ведётся в файле CHANGELOG.md.
NoteSearch использует системный журнал macOS. Приложение не пишет содержимое файлов и поисковые запросы в открытом виде: пути и запросы должны быть помечены как private-данные.
Посмотреть журнал за последние 10 минут:
log show --last 10m \
--predicate 'subsystem == "ru.vincento.NoteSearch"' \
--infoСледить за событиями в реальном времени:
log stream \
--predicate 'subsystem == "ru.vincento.NoteSearch"' \
--infoКатегории журнала:
index— первичная индексация, сверка и ошибки файлов;search— выполнение поиска;watch— FSEvents и обновления по изменениям файлов;ui— действия интерфейса;system— хоткей, окно и запуск вместе с macOS.
Если вы отправляете лог в issue, проверьте его содержимое: ошибки и пути могут быть видны в зависимости от режима просмотра Console.
Для распространения вне App Store (путь А) используется скрипт scripts/release.sh.
# Полный цикл: тесты + сборка + упаковка
bash scripts/release.sh
# Только сборка и упаковка (без тестов)
bash scripts/release.sh --skip-tests
# Сборка с конкретной версией (переопределяет MARKETING_VERSION из Xcode)
VERSION=1.0.1 bash scripts/release.sh- Запускает автотесты (
swift test). - Собирает
Release-конфигурацию только подarm64. - Подписывает приложение ad-hoc подписью.
- Проверяет подпись и Bundle ID.
- Упаковывает
.appв ZIP (dist/NoteSearch-<version>-arm64.zip). - Создаёт DMG с ярлыком на
/Applicationsи инструкцией. - Генерирует
dist/SHA256SUMS.txtс контрольными суммами.
dist/
├── NoteSearch-1.1.0-arm64.zip
├── NoteSearch-1.1.0-arm64.dmg
└── SHA256SUMS.txt
# Проверить контрольные суммы
cd dist
shasum -a 256 -c SHA256SUMS.txt
# Локально запустить собранное приложение
open build/release/Build/Products/Release/NoteSearch.app
# Открыть DMG и проверить содержимое
open dist/NoteSearch-1.1.0-arm64.dmgПервый публичный релиз: v1.1.0 (ad-hoc, arm64). Исходный код, ZIP и DMG доступны на странице Releases.
Это ad-hoc сборка без Developer ID и нотаризации. После скачивания на другом Mac macOS может потребовать:
- System Settings → Privacy & Security → Open Anyway для NoteSearch.
