Skip to content

About

Приложение для macOS. Поиск по тексту в одном выбранном каталоге

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

NoteSearch

Нативное приложение для macOS, которое мгновенно ищет по тексту заметок и документов в одном выбранном каталоге. Вызывается глобальным сочетанием клавиш, показывает список файлов и контекст найденного места с подсветкой, а Enter открывает файл в привычной программе.

Release Platform Swift License Changelog

Содержание

Назначение

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, а не вложенную папку.

Первый запуск

  1. По умолчанию индексируется ~/Documents/Notes. Если папки нет, она создаётся.
  2. Чтобы выбрать другой каталог: значок лупы в строке меню → «Настройки…» → «Каталог» → «Изменить…». Приложение предупредит, что старый индекс будет заменён.
  3. macOS может запросить доступ к папке «Документы». Разрешите, иначе читать файлы не получится.
  4. Дождитесь статуса «Готов к поиску» и вводите запрос.

Глобальное сочетание клавиш

По умолчанию задано ⌘⌥Space, но это сочетание в macOS по умолчанию занято системой («Показать окно поиска Finder»). Чтобы использовать его:

  1. System Settings → Keyboard → Keyboard Shortcuts… → Spotlight.
  2. Снимите галочку «Показать окно поиска Finder».
  3. Перезапустите NoteSearch.

Либо запишите другое сочетание: «Настройки → Окно → Глобальное сочетание», нажмите кнопку и введите новое (например, ⌃⌥⌘Space). Нужен хотя бы ⌘ или ⌃, Esc отменяет запись.

Использование

Горячие клавиши

Действие Сочетание
Показать или скрыть панель глобальное сочетание (по умолчанию ⌘⌥Space)
Фокус на поле поиска ⌘K
Обернуть запрос в кавычки (точная фраза) / снять кавычки ⌘'
Следующий результат ↓
Предыдущий результат ↑
Открыть файл Enter
Показать файл в Finder ⌘Enter
Очистить запрос Esc или ⌘⌫
Скрыть панель Esc при пустом запросе

После открытия файла панель скрывается. Она также скрывается при клике вне панели (отключается в настройках).

Примеры запросов

Запрос Что найдёт
docker compose документы, где есть оба слова (каждое по началу слова)
dock comp то же самое: слова ищутся по началу
"домашний сервер" точная фраза целыми словами
uv-proekt слово с дефисом, ищется как единое выражение
елка «ёлка» и «елка»

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

Строка меню

Значок лупы открывает меню: «Открыть поиск», «Настройки…», «Переиндексировать», «Выйти из NoteSearch». Если скрыть значок в Dock, закрыть приложение можно только через это меню.

Как работает поиск

Разбор запроса

Запрос разбирается в список терминов:

  • слово без кавычек: термин-префикс, превращается в FTS-условие "слово"*;
  • текст в кавычках: термин-фраза, превращается в "точная фраза" без звёздочки;
  • термины без букв и цифр (например, ---) отбрасываются;
  • термины соединяются через AND.

Пример: docker "домашний сервер" превращается в "docker"* AND "домашний сервер".

Нормализация

Текст файлов, имена и пути при индексации, а также термины запроса приводятся к одному виду:

  1. Юникод-нормализация NFC (разложенные буквы вроде и + знак краткости собираются в одну).
  2. Замена ё на е (и Ё на Е).
  3. Регистр убирает токенайзер 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. При смене версии индекс пересоздаётся автоматически.

Первичная индексация

  1. Рекурсивный обход выбранного каталога (скрытые файлы пропускаются).
  2. Проверка расширения (без учёта регистра) и исключений.
  3. Чтение файла как UTF-8; файлы больше 10 МБ и файлы, которые не читаются, пропускаются.
  4. Запись документа и его текста в индекс в одной транзакции.
  5. Прогресс в статусе; пропущенные файлы считаются и выводятся в статусе (Готов к поиску · пропущено: 1), причины пишутся в журнал.

Сверка при запуске

Индекс может отстать, если файлы менялись, пока приложение было закрыто. Поэтому при каждом запуске:

  1. Запускается отслеживание файловой системы (чтобы не потерять события).
  2. Состояние диска сравнивается с индексом по дате изменения и размеру.
  3. Новые файлы добавляются, изменённые перечитываются, пропавшие (а также ставшие исключёнными) удаляются.

Если каталога нет на месте (например, отключён диск), индекс не стирается.

Отслеживание изменений

Используется 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.

Release-сборка

Для распространения вне 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

Что делает скрипт

  1. Запускает автотесты (swift test).
  2. Собирает Release-конфигурацию только под arm64.
  3. Подписывает приложение ad-hoc подписью.
  4. Проверяет подпись и Bundle ID.
  5. Упаковывает .app в ZIP (dist/NoteSearch-<version>-arm64.zip).
  6. Создаёт DMG с ярлыком на /Applications и инструкцией.
  7. Генерирует 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.

About

Приложение для macOS. Поиск по тексту в одном выбранном каталоге

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages