A native desktop app (Mac / Windows) that takes drag-and-dropped rar files, zip files, and folders and batch re-archives them into zip files following a single naming rule. rar and zip files are extracted and re-compressed to standard Deflate zip; folders are zipped as-is.
Download the installer for your OS from the latest release:
- macOS β
simple-archiver_<version>_universal.dmg(universal: Apple Silicon + Intel) - Windows β
simple-archiver_<version>_x64-setup.exe(installer) orsimple-archiver_<version>_x64_en-US.msi
Important
The app is not notarized by Apple and not signed by a Windows publisher, so your OS shows a security warning the first time you open it. This is expected and safe. Follow the steps below once β after that, the app opens normally like any other app.
-
Double-click the downloaded
.dmg. If a dialog says "β¦dmg cannot be opened because Apple cannot check it for malicious software", click Done (do not click Move to Trash). -
Open the Apple menu β System Settingsβ¦ β Privacy & Security.
-
Scroll down to the Security section. macOS shows the blocked item with an Open Anyway button on the right, like this:
"simple-archiver_β¦_universal.dmg" was blocked to protect your Mac.
[ Open Anyway ]Apple could not verify "simple-archiver_β¦_universal.dmg" is free of malware that may harm your Mac or compromise your privacy.
Click the Open Anyway button on the right of that message.
-
Click Open Anyway once more to confirm, then unlock with Touch ID or your Mac password.
-
The
.dmgopens. Drag the simple-archiver icon onto the Applications folder shown in the window. -
Open simple-archiver from Applications (or Launchpad). If the same warning appears for the app itself, repeat steps 2β4 once β the blocked item will now be the app β and it will launch. It opens directly every time after that.
- Run the downloaded
.exe(or.msi). - If "Windows protected your PC" (SmartScreen) appears, click the small More info link.
- Click Run anyway, then follow the installer prompts.
Advanced (Terminal): skip the macOS warning
If you are comfortable with the Terminal, you can remove the quarantine flag instead of using System Settings:
# the downloaded disk image
xattr -d com.apple.quarantine ~/Downloads/simple-archiver_*_universal.dmg
# the installed app (after dragging it to Applications)
xattr -dr com.apple.quarantine /Applications/simple-archiver.appDrop a mix of rar files, zip files, and folders, reorder them, give the batch a single naming rule with an auto-incrementing sequence number, and hit run. Each item is compressed to a zip in list order, with a per-row progress bar and ETA plus an overall progress bar and ETA for the whole job.
It ships as a single self-contained native application built with Tauri 2 β a Rust engine for the archiving work and a Vite + React frontend for the UI.
- Drag & drop intake β add multiple rar files, zip files, and/or folders at once (drag-drop or a file dialog).
- Reorderable & selectable list β select rows (click, Shift+click for a range, Cmd/Ctrl+click to toggle), move items up and down (drag, buttons, or arrow keys), and delete unwanted rows; the run order follows the list top to bottom. See Keyboard shortcuts.
- Content-fit columns β the queue and the completion summary size each column to its content so every column stays visible without horizontal scrolling. In the queue you can still drag a column's right edge to set a custom width, or double-click the resize handle to snap it back to its content.
- Batch naming rule β specify a single prefix string and number every item sequentially from
1, top to bottom. A placeholder inside the prefix marks where the sequence number goes:{n}inserts the bare number, and{n:0W}zero-pads it to widthW(1β9), e.g.photo_{n:03}βphoto_001,photo_002, β¦ A live preview shows the resultingseq=1filename as you type, and every queue row previews its own output name. The sequence number is fixed at job-creation time in list order (independent of completion order). Output names are de-duplicated case-insensitively and validated to be Windows-safe. - One-click run β compresses every item per the naming rule, from the top of the list:
- rar file β extracted to a temporary workspace, then re-compressed into a standard Deflate zip.
- zip file β extracted to a temporary workspace, then re-compressed into a standard Deflate zip.
- folder β compressed into a zip directly.
- Live per-item progress β each row shows a progress bar and an estimated-time-remaining string, updated asynchronously while the job runs.
- Overall progress β a job-wide progress bar and ETA for all items combined.
- Output directory β pick one destination folder through the native OS picker; all archives are written there. The app remembers your last chosen directory and, on first run, defaults to the OS Downloads folder.
- Resilient by design β existing names are not overwritten (that item fails instead), a failed item never stops the others, and a run can be cancelled (in-flight work is interrupted and partial output / temp files are cleaned up). A run summary tallying succeeded / cancelled / failed items is shown at the end.
The queue supports both mouse selection and keyboard control. Click anywhere in the queue to focus it, then:
| Action | Shortcut |
|---|---|
| Select a single row | Click |
| Add / remove a row from the selection | Cmd/Ctrl + Click |
| Select a range of rows | Shift + Click |
| Select all rows | Cmd/Ctrl + A |
| Move the selected row up / down | β / β |
| Delete the selected rows | Delete / Backspace |
| Clear the selection | Esc |
Selection, reordering, and deletion are disabled while a job is running. Moving a row with the arrow keys requires exactly one selected row; with no row or several rows selected, the arrow keys scroll the queue instead.
Prerequisites: a Rust toolchain, Node.js, and pnpm. The toolchain versions are pinned via mise.toml (run mise install to get them), and the Tauri prerequisites for your OS are listed in the Tauri docs.
pnpm install # install frontend dependencies
pnpm tauri dev # run the app in development
pnpm tauri build # build a native bundle for the current OSRun all commands from the repo root (the Cargo workspace lives there, not under src-tauri/).
# Rust β pure core (the TDD battleground; -p keeps Tauri out)
cargo nextest run -p simple-archiver-core
cargo clippy -p simple-archiver-core --all-targets -- -D warnings
cargo fmt
# Frontend
pnpm test # Vitest one-shot
pnpm check # oxfmt + oxlint: format + lint with autofix
pnpm build # tsc + vite build (the load-bearing type gate)| Area | Choice |
|---|---|
| Framework | Tauri 2 β single native app for Mac / Windows |
| Frontend | Vite + React 19 + TypeScript (strict) + Tailwind CSS v4 + shadcn/ui (new-york) on Radix primitives (class-variance-authority + clsx + tailwind-merge) + lucide-react (icons) + zustand (state) + Inter (variable font) |
| Design | Base layout after shadcn-admin; design system after the shadcn.io ASICS design (ASICS color tokens, light/dark theme, Inter typography) |
| Backend / engine | Rust, DDD layered / hexagonal (Cargo workspace: pure simple-archiver-core crate + src-tauri presentation crate) |
| zip creation | async_zip |
| rar extraction | unrar (extract-only; bundled C++ source for both Mac / Windows) |
| Pluggable compression | Extractor / Archiver / Clock ports + a FormatRegistry that routes each source kind to the right adapter β see Pluggable compression |
| TypeScript bindings | ts-rs (#[derive(TS)] on DTOs generates the .ts wire contract in src/bindings/) |
| Naming parser | LALRPOP (grammar codegen) + logos (lexer) β build-time tooling inside simple-archiver-core |
| Rust tests | cargo-nextest (runner) + mockall (port mocks) + loom (concurrency verification) |
| Frontend tests | Vitest + Testing Library (jsdom) |
| Format / lint | oxlint + oxfmt (frontend) + cargo fmt / clippy (Rust) |
| Dead-code | knip (frontend, pnpm knip) |
| Tooling | pnpm (package manager) + mise (pinned toolchain) |
Technology choices are fixed. The compression libraries are deliberately kept behind a common interface so they can be treated as plugins (see below), but the libraries themselves are not swapped without a corresponding design change.
The spec calls for the compression libraries to be treated as plugins, behind a common interface, with the per-format differences isolated in one place. This is realized with:
- Common ports β
Extractor(rar β directory) andArchiver(directory β zip), so the engine drives any format through the same async interface. FormatRegistryβ resolves eachSourceItemto a compressible directory: aFolderis compressed directly, while aRarFileorZipFileis first extracted (viaUnrarExtractororZipExtractorrespectively) into aTempWorkspaceRAII guard and then re-compressed to standard Deflate zip. The guard'sDropguarantees temp cleanup even on error.
Adding another archive format means adding an adapter behind these ports and a branch in the registry β the engine, domain, and UI stay untouched.
The end-to-end path from a drop to the final summary, across the four layers:
sequenceDiagram
autonumber
actor User
participant UI as Frontend<br/>(React + zustand)
participant Cmd as Tauri commands<br/>(presentation)
participant Engine as RunArchiveJob + Aggregator<br/>(application)
participant Worker as Worker ΓN<br/>(per task)
participant Reg as FormatRegistry
participant Unrar as UnrarExtractor
participant Zip as ZipArchiver
Note over User,Cmd: 1 β Build the draft
User->>UI: drag & drop rar/zip files / folders
UI->>Cmd: add_items(paths)
Cmd->>Cmd: classify_path β Folder | RarFile | ZipFile
Cmd-->>UI: DraftSnapshot
User->>UI: set naming rule + output dir
UI->>Cmd: set_naming_rule / set_output_dir
UI->>Cmd: preview_output_name(template, seq)
Cmd-->>UI: preview filenames
Note over User,Zip: 2 β Run the job
User->>UI: click Run
UI->>Cmd: run_job()
Cmd->>Cmd: draft.build() β ArchiveJob, claim run slot
Cmd->>Engine: execute_with_cancellation(job, sink, token)
loop per task β bounded to N parallel workers
Engine->>Worker: spawn run_one(task)
alt RarFile
Worker->>Reg: prepare(RarFile)
Reg->>Unrar: extract(path) β temp workspace
Unrar-->>Worker: ExtractedTree (RAII temp guard)
else ZipFile
Worker->>Reg: prepare(ZipFile)
Reg->>Zip: extract(path) β temp workspace
Zip-->>Worker: ExtractedTree (RAII temp guard)
else Folder
Worker->>Reg: prepare(Folder) β dir as-is
end
Worker->>Zip: compress(dir, dest, ctx)
loop while compressing
Zip-->>Worker: ctx.report(bytes_done / total)
Worker-->>Engine: WorkerMsg::Progress (mpsc)
Engine->>Engine: Aggregator.apply + snapshot
Engine-->>UI: emit "archive://progress"
UI->>UI: applyProgress β per-row & overall bar / ETA
end
Worker-->>Engine: WorkerMsg::Status (TaskEvent: Complete | Fail | Cancel)
Note over Worker,Unrar: temp workspace dropped β cleaned up
end
Engine-->>Cmd: JobSummary
Cmd-->>UI: JobSummaryDto (run_job resolves)
UI-->>User: success / failure summary
Note over User,Zip: Cancellation β any time during a run
User->>UI: click Cancel
UI->>Cmd: cancel_job()
Cmd->>Engine: request_cancel() β CancellationToken fired
Engine-->>Worker: ctx.is_cancelled() β interrupt in-flight task
Workers run concurrently up to available_parallelism, but only one task β the aggregator on the engine's own task β ever writes progress: each worker pushes WorkerMsg over an mpsc channel, the aggregator folds them into a job-wide snapshot, and the presentation layer's TauriEmitter forwards each snapshot to the frontend as an archive://progress event. A failed item is tallied but never stops its siblings, and run_job always resolves with a JobSummaryDto β the load-bearing terminal signal β even if some progress frames were dropped along the way.
docs/architecture.mdβ layered / hexagonal design and layer boundariesdocs/development.mdβ tech stack, dev commands, testing policy, PR rulesCLAUDE.mdβ mandatory project rules and documentation map
This project is licensed under the MIT License.
