Вступление: проблема, которая надоела

Каждый день мы работаем с файлами: перемещаю, копируем, сортируем, раскладываем по папкам. И каждый день тратим массу времени на то, чтобы открыть нужную директорию, перетащить файл, закрыть лишние окна. В Windows эта рутина особенно утомляет: проводник, куча вкладок, постоянное переключение между папками. Хотелось простого решения: выделил файл → правый клик → выбрал папку → файл перемещён. Без лишних телодвижений, без открытия дополнительных окон.

Поискал готовые утилиты. Есть проприетарные решения — FastFolders, XYplorer, Directory Opus. Но они либо платные, либо перегружены функционалом, либо требуют сложной настройки. Бесплатные аналоги оказались либо неудобными, либо недостаточно гибкими. «А почему бы не написать свою?» Да и вообще иногда даже царям надо заборы красить! Это о рефлексии системного архитектора себя в мире.

К тому моменту уже много лет работал с C++ — знал WinAPI, COM, IPC, низкоуровневую работу с системой. Но в современных проектах хотелось попробовать что-то более безопасное, современное и приятное в разработке. Так обратил внимание на Rust. Безопасная работа с памятью, богатая экосистема, отличная поддержка Windows через windows-rs — это выглядело многообещающе. А Tauri (на базе Rust) давал возможность сделать красивый GUI на React, но без тяжеловесного Electron.

В итоге решил: изучу Rust на реальном проекте, который буду использовать сам. И если он окажется полезным — поделюсь с сообществом. Даже написал серию уроков, для самых маленьких, по rustlang.

Рождение QuickSort: от идеи к первому прототипу

Первая цель была простой и конкретной: написать утилиту, которая добавляет в контекстное меню проводника каскадное меню с избранными папками. Клик — и файл перемещён. Никаких лишних окон, никаких перетаскиваний.

Quicksort
Quicksort

Почему выбрал именно этот стек:

  • Rust — безопасность, производительность, кроссплатформенность (в будущем), отличная интеграция с Windows через windows-rs.

  • Tauri 2 — легковесный GUI, безопасная изоляция бэкенда и фронтенда, встроенная поддержка системных интеграций, минимальный размер приложения.

  • React + TypeScript — быстрая разработка интерфейса, компонентный подход, огромная экосистема.

Первые же шаги показали, что COM-интеграция с проводником Windows — это нетривиальная задача. Пришлось разбираться с устройством контекстного меню, интерфейсами IContextMenu, IShellExtInit, регистрацией COM-сервера без прав администратора. Rust + windows-rs сильно упростили работу с WinAPI, но потребовали времени на изучение. Перелопатил тонны документации, примеров, форумов, статей и исходников.

Отдельное спасибо авторам:

И тут на меня напал «нейрослоп».

Шутка, конечно. Но если серьёзно — без нейросетей бы потратил в разы больше времени. Локальные модели стали моими верными помощниками. Генерация шаблонов, поиск ошибок, перевод документации с «птичьего языка» на человеческий — нейросети ускорили процесс в несколько раз.

Но есть важный нюанс. «Нейрослоп» — это когда ИИ выдаёт код, который выглядит убедительно, компилируется, но делает не то, что надо. Или делает, но с багами, которые вылезают в самый неподходящий момент. Поэтому правило железное: нейросети — для скорости, проверка — руками. ИИ — это ассистент, а не замена разработчику.

В итоге через пару недель появился работающий context-menu-dll, который загружался в Explorer и показывал список папок. Ура! Прототип заработал.

Quicksort
Quicksort

Архитектура: не просто «ещё одна утилита»

Когда прототип начал работать, понял: можно остановиться, но хочется сделать что-то большее. Поэтому сразу заложил правильную архитектуру.

Я взял за основу Clean Architecture + DDD — подход, который хорошо зарекомендовал себя в корпоративных проектах. Разделение на слои:

  • Domain — сущности (File, Folder, Operation), value objects, события. Никаких внешних зависимостей.

  • Application — use cases (перемещение, копирование, отмена), порты (интерфейсы для внешних зависимостей).

  • Infrastructure — реализация репозиториев (пока JSON), файловой системы, генерация UUID.

  • Adapters — Tauri-приложение, COM-сервер, IPC-клиент.

Все крейты собраны в одном Cargo workspace. Это удобно для разработки и сборки.

COM-сервер реализован как независимая DLL. Она не требует прав администратора — регистрация в HKCU. При штатном завершении программы все ключи реестра удаляются, никакого мусора в системе не остаётся.

//! DLL lifecycle: owner-process liveness check.
//!
//! The DLL is loaded by Explorer.exe, not by our app. The app writes its PID
//! to `%APPDATA%/QuickSort/dll_owner.pid` on startup. The DLL reads that file
//! and checks whether the process is still alive.
//!
//! If the owner is dead the DLL refuses to create new COM objects
//! (`DllGetClassObject` → `CLASS_E_CLASSNOTAVAILABLE`) and tells COM it may
//! unload (`DllCanUnloadNow` → `S_OK`). Explorer will eventually release the
//! DLL.
//!
//! Emergency kill-switch: the `unregister_com_server` Tauri command removes
//! all registry entries and restarts Explorer, which force-unloads the DLL.

use std::fs;
use std::path::PathBuf;

use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Threading::{GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};

fn pid_file_path() -> Option<PathBuf> {
    let appdata = std::env::var("APPDATA").ok()?;
    Some(PathBuf::from(appdata).join("QuickSort").join("dll_owner.pid"))
}

fn read_owner_pid() -> Option<u32> {
    let content = fs::read_to_string(pid_file_path()?).ok()?;
    content.trim().parse().ok()
}

fn is_process_alive(pid: u32) -> bool {
    unsafe {
        let handle: HANDLE = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
            Ok(h) => h,
            Err(_) => return false,
        };
        let mut exit_code = 0u32;
        let ok = GetExitCodeProcess(handle, &mut exit_code);
        let _ = CloseHandle(handle);
        match ok {
            Ok(()) => exit_code == 259, // STILL_ACTIVE
            Err(_) => false,
        }
    }
}

/// Returns `true` when the Tauri app that owns this DLL is still running.
pub fn is_owner_alive() -> bool {
    let pid = match read_owner_pid() {
        Some(p) => p,
        None => return false,
    };
    is_process_alive(pid)
}

/// Returns `true` when COM may unload this DLL.
///
/// Either there are no instances, or the owner process is dead.
pub fn can_unload(instance_count: u32) -> bool {
    if instance_count == 0 {
        return true;
    }
    !is_owner_alive()
}

IPC через Named Pipes — простой JSON-фрейминг, асинхронная обработка команд. Это позволяет обмениваться данными между COM-сервером, GUI и фоновым сервисом (который появится позже).

//! Windows Shell Extension (COM) for QuickSort.
//!
//! This DLL is loaded by Explorer.exe and provides a cascading context menu.
//! It communicates with the main Tauri app via Named Pipe.

use std::cell::RefCell;
use std::ffi::{c_void, CStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::OnceLock;
use std::{mem, ptr};

use quicksort_ipc_contract::OverwritePolicy;

use parking_lot::Mutex;
use windows::core::{
    implement, w, IUnknown, Interface, Ref as WinRef, Result as WinResult, BOOL, GUID, HRESULT,
    PCWSTR, PSTR, PWSTR,
};
use windows::Win32::Foundation::{
    CLASS_E_NOAGGREGATION, E_FAIL, E_NOINTERFACE, E_NOTIMPL, E_POINTER, HWND, LPARAM, S_OK,
};
use windows::Win32::System::Com::{
    IClassFactory, IClassFactory_Impl, IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL,
};
use windows::Win32::System::Memory::GlobalLock;
use windows::Win32::System::Ole::{ReleaseStgMedium, CF_HDROP};
use windows::Win32::System::Registry::HKEY;
use windows::Win32::UI::Shell::{
    Common::ITEMIDLIST, IContextMenu, IContextMenu_Impl, IShellExtInit, IShellExtInit_Impl,
    SHBrowseForFolderW, SHGetPathFromIDListW, BIF_RETURNONLYFSDIRS, BROWSEINFOW, CMF_DEFAULTONLY,
    CMINVOKECOMMANDINFO, DROPFILES, GCS_VALIDATEA, GCS_VALIDATEW,
};
use windows::Win32::UI::WindowsAndMessaging::{
    CreatePopupMenu, InsertMenuItemW, HMENU, MENUITEMINFOW, MFS_ENABLED, MFT_SEPARATOR,
    MIIM_BITMAP, MIIM_FTYPE, MIIM_ID, MIIM_STATE, MIIM_STRING, MIIM_SUBMENU,
};

use crate::icon;
use crate::pipe_client::{move_to_folder, select_folder};

/// Cached app icon bitmap for MIIM_BITMAP on the root "QuickSort" menu entry.
/// Loaded once on first use.
static APP_ICON_BITMAP: OnceLock<Option<usize>> = OnceLock::new();

fn get_app_icon_bitmap() -> Option<windows::Win32::Graphics::Gdi::HBITMAP> {
    let opt = APP_ICON_BITMAP
        .get_or_init(|| icon::load_app_icon_bitmap().map(|bmp| bmp.0.expose_provenance()));
    opt.map(|addr| windows::Win32::Graphics::Gdi::HBITMAP(ptr::with_exposed_provenance_mut(addr)))
}

// ============================================================================
// Logging initialization
// ============================================================================

static LOG_INIT: OnceLock<()> = OnceLock::new();

fn init_logging() {
    LOG_INIT.get_or_init(|| {
        let log_dir = match std::env::var("APPDATA") {
            Ok(appdata) => {
                let mut p = std::path::PathBuf::from(appdata);
                p.push("QuickSort");
                let _ = std::fs::create_dir_all(&p);
                p.push("quicksort_dll.log");
                p
            }
            Err(_) => std::env::current_exe()
                .unwrap_or_default()
                .with_file_name("quicksort_dll.log"),
        };

        if let Ok(file) = std::fs::File::create(&log_dir) {
            let config = simplelog::ConfigBuilder::new()
                .add_filter_allow_str("context_menu_dll")
                .build();
            let _ = simplelog::WriteLogger::init(simplelog::LevelFilter::Debug, config, file);
            log::info!("DLL logging started.");
        }
    });
}

// ============================================================================
// COM class: QuickSortShellExt
// ============================================================================

pub static INSTANCE_COUNT: AtomicU32 = AtomicU32::new(0);
pub const CLSID_QUICKSORT: GUID = GUID::from_u128(0x12345678_1234_1234_1234_1234567890AB);

// Simple folder struct for menu building
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MenuFolder {
    id: String,
    name: String,
    path: String,
    is_favorite: bool,
    color: Option<String>, // e.g. "#FF5733"
}

#[implement(IShellExtInit, IContextMenu)]
pub struct QuickSortShellExt {
    item_paths: RefCell<Vec<PathBuf>>,
    folders: Mutex<Vec<MenuFolder>>,
    min_cmd_id: std::cell::Cell<u32>,
}

impl Default for QuickSortShellExt {
    fn default() -> Self {
        INSTANCE_COUNT.fetch_add(1, Ordering::SeqCst);
        init_logging();

        Self {
            item_paths: Default::default(),
            folders: Mutex::new(Vec::new()),
            min_cmd_id: std::cell::Cell::new(0),
        }
    }
}

impl Drop for QuickSortShellExt {
    fn drop(&mut self) {
        INSTANCE_COUNT.fetch_sub(1, Ordering::SeqCst);
    }
}

// ============================================================================
// IShellExtInit implementation
// ============================================================================

impl IShellExtInit_Impl for QuickSortShellExt_Impl {
    fn Initialize(
        &self,
        _folder_idl: *const ITEMIDLIST,
        data_obj: WinRef<'_, IDataObject>,
        _prog_id: HKEY,
    ) -> WinResult<()> {
        log::info!(
            "IShellExtInit::Initialize called (folder_idl present: {}, has_data_obj: {})",
            !_folder_idl.is_null(),
            data_obj.as_ref().is_some()
        );
        let paths = match data_obj.as_ref() {
            Some(obj) => match extract_files_from_dataobject(obj) {
                Ok(p) => p,
                Err(e) => {
                    log::error!("Failed to extract files from IDataObject: {:?}", e);
                    return Err(e);
                }
            },
            None => {
                log::warn!("IDataObject is null — proceeding with empty selection");
                Vec::new()
            }
        };
        log::info!("Initialize: got {} paths", paths.len());
        self.this.item_paths.replace(paths);
        Ok(())
    }
}

// ============================================================================
// Helper functions for file extraction
// ============================================================================

unsafe fn dropfiles_to_paths(files: &DROPFILES) -> Vec<PathBuf> {
    let mut res = Vec::new();
    let is_wide = files.fWide.as_bool();
    let mut str_ptr = files as *const DROPFILES as *const u8;
    str_ptr = str_ptr.add(files.pFiles as usize);

    loop {
        if is_wide {
            if *(str_ptr as *const u16) == 0 {
                break;
            }
        } else {
            if *str_ptr == 0 {
                break;
            }
        }

        let (bytes_shift, path) = if is_wide {
            let s = PCWSTR(str_ptr as *const u16);
            let len = s.len();
            (
                2 * (len + 1),
                PathBuf::from(OsString::from_wide(s.as_wide())),
            )
        } else {
            let s = CStr::from_ptr(str_ptr as *const i8);
            let bytes = s.to_bytes();
            (
                bytes.len() + 1,
                PathBuf::from(String::from_utf8_lossy(bytes).into_owned()),
            )
        };
        res.push(path);
        str_ptr = str_ptr.add(bytes_shift);
    }
    res
}

Что дал Rust?

  • windows-rs — безопасные обёртки над WinAPI, компилятор не даёт стрелять себе в ногу.

  • serde — удобная сериализация для IPC и хранения конфигов.

  • Встроенное тестирование и документация — приятный бонус.

  • Borrow checker — поначалу бесил, но потом спас от многих трудноуловимых багов.

Ключевые фичи: что умеет QuickSort прямо сейчас

Каскадное контекстное меню — главная «фишка», ради которой всё и затевалось. Вложенность до 10 уровней, настройка прямо из GUI. Выделил файл, правый клик, выбрал папку — и файл уже там.

Мгновенное перемещение/копирование — атомарные операции с поддержкой跨дисковых перемещений. Предварительная проверка: есть ли дубликаты? (по имени, размеру, SHA-256). Если дубликат найден — уведомление, чтобы случайно не перезаписать важный файл.

История операций и отмена (Undo) — полный аудит всех действий. Можно откатить перемещение или копирование одним кликом. Это спасло меня уже не раз, когда случайно перемещал файл не в ту папку.

Чистый и удобный GUI — редактор папок, поиск по библиотеке, тёмная тема, системный трей. Ничего лишнего — только то, что нужно для управления папками и операциями.

Zero-install deployment — один .exe-файл, который при первом запуске сам регистрирует COM-сервер. Портативный режим: можно запускать с флешки, никаких следов в системе (кроме временных записей в реестре, которые удаляются при выходе).

Что дальше? Планы, которые меня вдохновляют

На данный момент QuickSort — рабочий прототип для Windows 10/11 x64. Уже есть всё, что перечислил выше. Но амбиции — намного шире.

Ближайшие планы (Фаза 0–2):

  • Кроссплатформенность (macOS, Linux) — адаптация путей, IPC через Unix-сокеты, интеграция с Nautilus/Finder.

  • Интерактивная командная строка с синтаксисом Everything — поиск, фильтрация, сортировка.

  • Расширенная индексация (USN Journal / inotify / FSEvents).

Долгосрочные амбиции (Фаза 3–11):

  • Семантическая организация папок (sidecar-файлы, теги, автоматическая категоризация).

  • Плагинная система: поддержка плагинов Total Commander (WCX/WDX/WFX/WLX) + Python через MCP + WASM.

  • Локальный ИИ (ONNX) — автоматическое тегирование, семантический поиск, категоризация.

  • Децентрализованный реестр «Смыслобайт» — неизменяемая фиксация хэшей и метаданных в блокчейне.

  • Фоновый сервис (Windows Service / Linux Daemon) для выполнения задач без GUI.

Проект полностью Open Source (MIT), и открыт к любым contributions — от баг-репортов до pull request’ов.

Главные уроки: что вынес из этого опыта

Rust — это не страшно. После C++ кривая обучения оказалась довольно пологой. Да, borrow checker поначалу раздражал, но потом понял, что это не ограничение, а защита. Компилятор ловит ошибки, которые в C++ вылезли бы в рантайме.

Tauri — отличная альтернатива Electron. Минимальный оверхед, нативный бэкенд на Rust, безопасный фронтенд. Идеально подходит для утилит, которым нужен современный GUI, но не нужен лишний вес.

Clean Architecture окупается. Даже для небольшого проекта разделение на слои упрощает тестирование и расширение. Чёткие границы между доменом, приложением и инфраструктурой позволяют безболезненно менять реализации (например, перейти с JSON на SQLite).

Нейросети — это инструмент, а не решение. Они ускоряют разработку, но без контроля превращаются в «нейрослоп». Самый ценный навык сейчас — не знание языка или фреймворка, а умение тестировать, документировать, проектировать и анализировать. Код — это лишь часть работы.

Open Source — это круто. Проект живёт, даже когда у тебя нет времени — сообщество помогает с багами, идеями и переводом документации. Приятно видеть, что твоя утилита кому-то реально упрощает жизнь.

Quicksort
Quicksort

Заключение: попробуйте, поучаствуйте, помогите сделать лучше

QuickSort родился из личной потребности и желания изучить Rust. Вырос в полноценный проект с чистой архитектурой, открытым кодом и амбициозными планами. Мы продолжаем развивать его, добавлять фичи, расширять платформы.

Попробуйте QuickSort — скачать можно с GitHub: https://github.com/azmrv/QuickSort

Если понравилось — поставьте звезду ⭐, это лучшая мотивация для разработчиков.
Если хотите поучаствовать — создайте issue, мы обсудим и исправим, форкайте и отправляйте PR.
Помощь с документацией, переводами и тестированием тоже приветствуется!

Полезные ссылки:

P.S. А пока пошёл править баги, которые нейросеть написала вчера ночью. Но это уже совсем другая история.

PP.S. Технически насыщенная часть о разработке в процессе написания!

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


  1. tot0ro
    30.08.2026 15:06

    Статья нейрослоп или автор ( который много лет работал с c++ ) не знает про MC и прочие.


    1. AzKanzler Автор
      30.08.2026 15:06

      Привет! Спасибо за вопрос — он как раз про то, что многих интересует.

      Насчёт «много лет работал с C++» — да, это правда. Я хорошо знаком с WinAPI, COM, MFC, ATL, и даже с тем, как писать Shell Extensions «на чистых» C++ без обёрток. Более того, первая версия прототипа (чисто для себя) была написана на C++ с ATL — она работала, но код был громоздким, а поддержка — мучительной.

      Почему я ушёл в Rust? Не потому что не знаю C++, а потому что:

      • Rust даёт безопасность — borrow checker ловит ошибки на этапе компиляции, которые в C++ вылезли бы в рантайме (и иногда ловил).

      • Экосистема Rust для Windows (windows-rs) — это не просто обёртки, а безопасные обёртки, сгенерированные по метаданным WinRT. Это удобнее, чем ручная работа с GUID и HRESULT.

      • Tauri — даёт современный GUI на React без Electron-тяжеловесности. На C++ пришлось бы либо писать на Qt (тяжело), либо на MFC (устарело), либо на Win32 (долго и больно).

      А про «нейрослоп» — это ирония. Без нейросетей я бы копался в документации неделями. Но они не пишут за меня — они помогают быстрее генерировать шаблоны и искать ошибки. В итоге проверяю всё руками, отсюда и шутка.

      Если вы имели в виду «MC» как MFC — то нет, MFC я не использовал, потому что это legacy-технология, которая не даёт кроссплатформенности и современного UI. Если вы имели в виду что-то другое — уточните, буду рад обсудить.

      Спасибо за комментарий — он даёт повод объяснить мотивацию подробнее! Буду рад услышать конкретные замечания по коду или архитектуре — открывайте issue на GitHub.


      1. AndreyDmitriev
        30.08.2026 15:06

        Если бы и коммент был нейрослопом, то он бы подсказал, что под МС имеется ввиду Midnight Commander. Его аналог под виндой — Far Manager, который избавляет от необходимости возиться с контекстным меню (ну, меня вов всяком случае). Я, кстати, не поленился и попробовал в виртуалке, и что-то как-то оно не очень работает. В контекстном меню пункт появляется, но при выборе пути не происходит ровным счётом ничего.

        Возможно потом в код гляну. Win11 LTSC с немецкой локалью, если что.


        1. AzKanzler Автор
          30.08.2026 15:06

          Привет, virtual box ubuntu latest аналогично да знаю где надо поправить, спасибо!
          Windows 10 все - Ok
          Причем тут mc это отдельное приложение совсем для других целей, а главное способов работы.