Как я написал раскладчик файлов, который ничего не удаляет и всё возвращает обратно

Рабочий стол зарастает быстро: скриншоты, скачанные ролики, договоры, установщики, архивы «для друга». Разбирать это руками скучно, а программы-сортировщики пугают: непонятно, куда они денут файлы и как потом всё вернуть.
Готовые решения есть. На Маке давно существует Hazel, на Windows есть DropIt и File Juggler. Но они работают по правилам, которые сначала надо написать самому, и почти никогда не объясняют, почему файл попал именно сюда.
Я сделал «Порядок»: бесплатную программу для Мака и Windows с открытым кодом. Она сама предлагает раскладку, показывает план с причиной для каждого файла, ничего не удаляет и одной кнопкой возвращает всё как было.
Что умеет
Раскладывает Рабочий стол, Загрузки, Документы или любую папку, которую перетащили на окно.
Сначала показывает план: куда поедет каждая вещь и почему («в названии «договор»», «в тексте «акт выполненных работ»», «формат .pdf»).
Узнаёт слова с окончаниями и латиницей: «смета» найдёт «сметы», «договор» найдёт
dogovor_arendy.pdf.Если название ничего не говорит, читает начало текста в PDF, DOCX и RTF, а на картинках распознаёт текст встроенным в систему движком.
Скриншоты и фото с телефона раскладывает по месяцам, у «отчёт (1).pdf» убирает «(1)», папке «Новая папка» с фото даёт имя с датой.
Точные копии, пустые файлы и установщики складывает в папку «На удаление». Удаляете их вы сами.
Не трогает папки, в которых сейчас работает программа, папку скриншотов, папки, нужные скриптам, и проекты с кодом.
Возвращает всё обратно, даже если раскладку прервал сбой питания.


Цифры
Замерено 01.11.2026 на MacBook с Apple Silicon.
Что | Сколько |
|---|---|
Размер программы для Мака | 5,3 МБ |
План для Рабочего стола, Загрузок и Документов, только по названиям | 0,15 с |
То же с чтением текста файлов | 8,0 с (из них 5 с ушло на ожидание системного распознавания, см. грабли №4) |
Чтение текста из PDF на 4 страницы | 97 мс |
Тестов ядра | 43, плюс сверка с ручной разборкой |
Случайных рабочих столов в проверке «разложить + вернуть» | 150 |
Грабли №1. Защита не узнавала папки с русскими именами
Симптом. Программа должна оставлять в покое папку, в которой сейчас открыт терминал или редактор. Папку tekst она узнавала, а папку «Чисто» нет.
Причина. Кто где работает, я узнаю у системы командой lsof -a -d cwd -Fcn: она выводит рабочие папки всех процессов. Всё, что не ASCII, lsof печатает кодами байтов:
n/Users/me/Desktop/\xd0\xa7\xd0\xb8\xd1\x81\xd1\x82\xd0\xbe/tekst
Строка \xd0\xa7… никогда не совпадёт с путём «/Users/me/Desktop/Чисто».
Решение. Собираю байты обратно и только потом читаю строку как UTF-8:
fn unescape_lsof(s: &str) -> String {
let b = s.as_bytes();
let mut out = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'\\' && i + 3 < b.len() && b[i + 1] == b'x' {
if let Ok(byte) = u8::from_str_radix(&s[i + 2..i + 4], 16) {
out.push(byte);
i += 4;
continue;
}
}
out.push(b[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}

Грабли №2. iCloud скачивает файлы, если на них просто посмотреть
Симптом. Если включены «Рабочий стол и Документы в iCloud», часть файлов лежит только в облаке: на диске есть имя и размер, а содержимого нет. Стоит программе прочитать хотя бы 12 байт (я так отличаю ярлык Finder от обычного файла), и система скачивает файл целиком.
Причина. Такие файлы помечены флагом SF_DATALESS. Чтение поднимает их из облака прозрачно для программы.
Решение. Флаг проверяется до любого чтения. Облачные файлы не читаются никогда, исключение сделано только для крошечных скриптов (до 64 КБ), чтобы найти в них пути:
const SF_DATALESS: u32 = 0x4000_0000;
meta.st_flags() & SF_DATALESS != 0
Отдельно проверил перенос с помощью ls -lO. Файл, переехавший со стола в Документы, так и остался dataless: обе папки синхронизирует iCloud, и это просто переименование. Файл, переехавший в Фильмы, потерял флаг, то есть скачался. Поэтому план считает, сколько придётся скачать, только для вещей, которые уезжают за пределы облачных папок, и пишет это заранее.
Грабли №3. Перенос и запись в журнал — это два действия
Симптом. Откат работает по журналу. Если писать в журнал после переноса и выключить компьютер между этими шагами, файл переедет, а журнал о нём не узнает.
Решение. Сделал журнал так, как это устроено в базах данных. Сначала пишется намерение, потом выполняется перенос, потом ставится отметка, и каждая строка сразу сбрасывается на диск:
{"op":"intent","id":3,"src":"/Users/me/Desktop/фото.jpg","dst":"/Users/me/Pictures/Картинки/фото.jpg"}
{"op":"done","id":3}
При откате намерение без отметки проверяется по диску: если на новом месте файл есть, а на старом нет, значит, переезд успел случиться, и его нужно вернуть. Оборванная последняя строка просто пропускается. Тест имитирует такой сбой: записывает два намерения, переносит один файл и обрывает журнал на середине строки.
Остальные правила простые. Используется только rename, никаких «скопировать и удалить». Занятое имя не перезаписывается: перед переносом имя проверяется ещё раз, потому что его могли занять, пока человек смотрел на план.
Грабли №4. Системное распознавание текста зависло на 25–50 секунд
Симптом. Текст на картинках я распознаю тем, что уже встроено в систему: Vision на Маке и Windows.Media.Ocr на Windows. Ставить ничего не нужно, и всё работает без сети. На моём Маке распознавание одной картинки занимало от 25 до 50 секунд и заканчивалось ошибкой e5rt_execution_stream_operation_create_precompiled_compute_operation_with_options call failed.
Причина. Ошибка в самом системном движке: маленькая проверочная программа на Swift падает точно так же. Быстрый режим работает, но русский язык он не поддерживает.
Решение. Предохранитель. Распознаванию даётся не больше 5 секунд. Если система не ответила, распознавание выключается до конца работы программы, и план строится по названиям:
match rx.recv_timeout(PATIENCE) {
Ok(text) => text,
Err(_) => {
BROKEN.store(true, Ordering::Relaxed);
None
}
}
Грабли №5. Пустая папка, которая весит 519 МБ
Симптом. Пустые папки программа отправляет в «На удаление». На проверке она собралась отправить туда папку резервной копии весом 519 МБ.
Причина. Внутри лежала только скрытая папка с данными программы, а скрытые файлы я при обходе пропускал. Для программы папка выглядела пустой.
Решение. Скрытые файлы теперь считаются отдельно (кроме служебных .DS_Store). Папка, в которой есть только скрытое, остаётся на месте с пояснением «похоже на данные программы». Эту ошибку нашла сверка с ручной разборкой, о ней ниже.
Как проверить, что раскладка разумная
Тестами на «правильно и неправильно» здесь не обойтись: правильно — это как разложил бы человек. Поэтому я разобрал настоящий Рабочий стол руками и записал каждое решение: чем смотрел, что заметил, куда положил и почему. Получилось 41 запись (имена личных проектов и клиентов заменены). Тест воссоздаёт этот стол в песочнице и сверяет решения программы с ручными.
Программа совпала со мной во всех 25 общих случаях. Ещё 16 решений опирались на то, что знает только хозяин компьютера: «этот архив относится к такому-то проекту», «это личные документы». Для них в программе есть свои правила, rules.json.
Вторая проверка — свойство «ничего не случилось». Тест 150 раз собирает случайный Рабочий стол с одинаковыми именами, копиями и пустыми файлами, раскладывает его и откатывает. После раскладки ни один файл не должен пропасть или оказаться задавленным, а после отката дерево папок должно совпасть с исходным байт в байт.
Окно-визитка
Окно сделано в пропорциях визитки 85 × 55 мм. Его ширина составляет малую долю золотого сечения экрана (38 %, но не меньше 540 и не больше 760 точек). По вертикали окно стоит чуть выше середины: свободное место делится тем же сечением. Кегль текста равен ширине окна, делённой на 36, поэтому на самом маленьком окне это 15 пикселей.
Шрифт EB Garamond. Курсив в первой версии плохо читался, и от него пришлось отказаться. Цифры в плане ровные, по линии строки. Кнопки сделаны текстом с подчёркиванием, рамок и плашек нет.
Что хранится
Журналы переносов:
~/Library/Application Support/poryadok/journalна Маке,%APPDATA%\poryadok\journalна Windows. В них только пути.Свои правила, если вы их завели:
rules.jsonтам же.Больше ничего. Программа не ходит в сеть, аккаунтов и аналитики нет. Текст файлов читается только для выбора полки и нигде не сохраняется.
Чем отличается
Цены проверены на сайтах 01.11.2026.
Порядок | Hazel 6 | DropIt | File Juggler | |
|---|---|---|---|---|
Цена | бесплатно | $42, на 5 Маков $65 | бесплатно | платно после 30 дней пробы |
Системы | Мак, Windows | Мак | Windows | Windows |
Открытый код | да, MIT | нет | да, GPLv2 | нет |
С чего начинается работа | сразу предлагает раскладку | правила пишете сами | правила пишете сами | правила пишете сами |
Эти программы умеют больше меня в одном: они следят за папками в фоне и срабатывают сами. Я сделал другое. Программа без настройки предлагает раскладку целиком, для каждого файла объясняет, почему он едет именно сюда, защищает папки, нужные другим программам, и одной кнопкой возвращает всё, даже после сбоя.
Чего не умеет
Не понимает смысла так, как человек. «Договор с клиентом» и «договор аренды квартиры» для неё одно и то же, пока вы не заведёте своё правило.
Не переносит проекты с кодом. В них бывают абсолютные пути, и после переноса проект может сломаться.
Не переносит файлы между дисками: она только переименовывает и никогда не копирует.
Не следит за папками в фоне. Раскладка запускается, когда вы сами нажмёте кнопку.
Программа не подписана платными сертификатами Apple и Microsoft, поэтому при первом запуске система предупредит. Как открыть, написано в README.
Ссылки
Установщики для Мака и Windows: https://github.com/kozliq/poryadok/releases/latest
Будет ещё.
Раньше в серии: Качалка — видео без водяных знаков, «Чисто» — без рекламы, Кружок — кружок из любого видео, Толково — голосовой ввод без интернета, Мимо — пропуск рекламных вставок в TikTok.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.