A local-first desktop application for browsing, downloading, and reading Islamic texts from the Shamela library. Built with Tauri 2, React 19, and Rust.
Libaby downloads the Shamela library catalogue from a private HuggingFace dataset and lets you browse all books, download individual books locally, and read them with full bilingual (Arabic + English transliteration) support. Everything is stored on disk — no external server is required after the initial download.
| Layer | Technology |
|---|---|
| Desktop shell | Tauri 2 |
| Frontend | React 19, TypeScript, Vite 8, Bun |
| UI components | shadcn/ui (Radix UI + Tailwind CSS) |
| Backend / IPC | Rust (src-tauri/src/) |
| Package manager | Bun ≥ 1.3.10 |
| Rust toolchain | Stable (via rustup) |
| Data source | HuggingFace private dataset (brotli-compressed JSON) |
- Browse Shamela catalogue — search across 10 000+ books with Arabic diacritic normalisation, macron stripping, and numeric ID lookup.
- Bilingual interface — toggle between English transliteration and Arabic. All book names, authors, and categories are shown in both scripts.
- Offline reading — books are downloaded once, stored as JSON, and read locally. No network needed after download.
- Translation support — if an English translation file exists on HuggingFace (
books/en/{id}.json.br), it can be downloaded and shown alongside the Arabic text in a two-column layout. - Citation copy — each translated page has a Copy button that formats the text + Arabic-Indic numerals citation + Shamela URL to the clipboard.
- Sidebar table-of-contents — books are expanded in the sidebar with their chapter/section tree for quick navigation.
- Bun ≥ 1.3.10 — bun.sh
- Rust stable —
rustup toolchain install stable - Tauri system deps — tauri.app/v2/guides/prerequisites (platform-specific)
- HuggingFace access token — read permission to the private Shamela dataset
- Shamela HuggingFace dataset name — e.g.
ragaeeb/shamela
git clone https://github.com/ragaeeb/libaby.git
cd libaby
bun install
bun run dev # starts Vite + Tauri dev windowOn first launch:
- Open Settings and enter your HuggingFace token and dataset name.
- Go to the Dashboard and click Download Master Database (≈40 MB, downloads
master/master.json.br+master/master_en.json.br). - Browse books in Shamela → Books, click a book, then Download Book.
- Click View Pages to read. If a translation is available, Download Translation will appear.
| Command | Description |
|---|---|
bun run dev |
Start Vite dev server + Tauri window |
bun run build |
Production Tauri build |
bun run vite:build |
TypeScript check + Vite bundle only |
bun run ci |
Vite build + cargo check (used in CI) |
bun run lint |
Biome lint |
bun run lint:fix |
Biome lint with auto-fix |
bun run format |
Biome format check |
bun run format:fix |
Biome format with auto-fix |
bun run upgrade:tauri |
Sync Tauri NPM + Rust crate versions (see below) |
libaby/
├── src/ # React frontend
│ ├── components/
│ │ ├── application-shell1.tsx # Root shell: routing, sidebar, breadcrumbs
│ │ ├── page-layout.tsx # Shared page wrapper
│ │ └── ui/ # shadcn/ui primitives
│ ├── lib/
│ │ ├── book-resource.ts # BookResource builder (pages, titles, index)
│ │ ├── book-resource-store.ts # LRU cache for BookResource instances
│ │ ├── book-translation.ts # EnExcerpt types, TranslationIndex builder, Tauri wrappers
│ │ ├── huggingface.ts # All Tauri command wrappers (invoke calls)
│ │ ├── io.ts # File I/O helpers
│ │ ├── shamela-content.tsx # Arabic content → markdown → React renderer
│ │ ├── shamela-tree.ts # Title node tree builder for sidebar
│ │ └── utils.ts # cn() and misc helpers
│ ├── pages/
│ │ ├── DashboardPage.tsx # Stats + download master
│ │ ├── SettingsPage.tsx # HuggingFace token + dataset config
│ │ ├── ShamelaPage.tsx # Paginated books table with search + language toggle
│ │ ├── BookDetailPage.tsx # Book info + download book/translation buttons
│ │ ├── BookPagesPage.tsx # Paginated table of all pages in a book
│ │ └── BookPageView.tsx # Single page reader with bilingual layout + citation
│ ├── stores/
│ │ ├── useSettingsStore.ts # Zustand: HF token, dataset, validation state
│ │ ├── useBooksStore.ts # Zustand: downloaded book IDs cache
│ │ └── useBookContentStore.ts # Zustand: in-memory book content cache
│ └── types/
│ └── books.ts # DenormalizedBook (extends shamela npm types with en_* fields)
│
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Tauri entry point
│ │ ├── lib.rs # All Tauri commands + app state
│ │ └── catalog.rs # MasterIndex: search, normalisation, EN injection
│ └── Cargo.toml # Rust dependencies
│
├── scripts/
│ ├── upgrade-tauri.sh # Sync Tauri NPM + Rust crate versions
│ └── sync-version.mjs # Sync package.json version → Cargo.toml
│
└── docs/ # Architecture notes and comparisons
HuggingFace dataset (private)
└─ master/master.json.br → master.json (local)
└─ master/master_en.json.br → master_en.json (local)
└─ books/{id}.json.br → books/{id}.json (local)
└─ books/en/{id}.json.br → books/en/{id}.json (local, optional)
Rust (catalog.rs)
MasterIndex { books: Vec<DenormalizedBook>, search_entries: Vec<SearchEntry> }
↳ builds normalised search blob per book (strips diacritics/macrons, includes numeric ID)
↳ injects en_name / en_author / en_category from master_en.json at index build time
Tauri IPC (lib.rs commands)
↳ query_master_books(params) → paginated + filtered book list
↳ download_and_cache_book(id) → fetches books/{id}.json.br, stores locally
↳ read_cached_book_if_exists(id) → reads books/{id}.json
↳ download_and_cache_book_translation(id) → fetches books/en/{id}.json.br
↳ read_cached_book_translation_if_exists → reads books/en/{id}.json
React frontend
↳ huggingface.ts wraps every invoke() call with typed promises
↳ book-resource.ts parses raw BookData → BookResource (pageById, pageIndexById, titleTree)
↳ book-resource-store.ts LRU cache (max 6) so repeat page navigations skip re-parsing
↳ book-translation.ts parses translation JSON → TranslationIndex (Map<pageId, EnExcerpt[]>)
The shell uses a discriminated union Route type. Navigation is purely in-memory — no URL router. enrichRoute fills in missing bookTitle/bookArTitle from knownBookTitles when navigating from the sidebar.
type Route =
| { page: "dashboard" }
| { page: "settings" }
| { page: "shamela-books" }
| { page: "shamela-book"; bookId: number; bookTitle?: string; bookArTitle?: string }
| { page: "shamela-book-pages"; bookId: number; bookTitle?: string; bookArTitle?: string }
| { page: "shamela-book-page"; bookId: number; pageId: number; pageNumber?: string | number;
bookTitle?: string; bookArTitle?: string }useEffectEventfunctions must never be inuseEffectdeps arrays — they are not stable references in all React builds. Only primitive values (strings, booleans, numbers) should be deps.bookMetais memoized inBookPageViewWrapper— preventsBookPageViewre-rendering on every parent render, which would re-run expensiveShamelaContentmarkdown parsing.- Translation index is keyed by
bookId, notpageId— the translation effect fires once per book, not on every page navigation. setKnownBookTitlesbails out when nothing changes — returnscurrentreference to avoid re-renders whenrefreshDownloadedBookspolls with unchanged data.
{
"headings": [{ "id": "A55", "text": "Muḥammad ibn Ismāʿīl al-Bukhārī" }],
"footnotes": [{ "id": "C23", "text": "Hadith" }],
"excerpts": [{ "id": "B1681", "text": "Ṣaḥīḥ al-Bukhārī" }]
}A{n}→authors.id = nC{n}→categories.id = nB{n}→books.id = n
Parsed by the shamela npm package (BookData type). Contains pages[] and titles[].
Same shape as master_en.json. Key field: excerpts[].from / excerpts[].to — page IDs from the Arabic book's pages[].id. Used to build TranslationIndex.
Tauri requires its NPM package (@tauri-apps/api) and Rust core crate (tauri) to be on the same major.minor version. When you run bun update, the NPM side can advance ahead of the Rust lock file and trigger:
Error Found version mismatched Tauri packages.
tauri (v2.x.y) : @tauri-apps/api (v2.z.0)
One-command fix:
bun run upgrade:tauriThis script (scripts/upgrade-tauri.sh) will:
- Upgrade
@tauri-apps/apiand@tauri-apps/cliviabun update. - Read the resolved
@tauri-apps/apiversion. - Patch
src-tauri/Cargo.tomlwith the matchingmajor.minorfor thetauricrate. - Run
cargo update.
Note:
tauri-buildandtauri-plugin-*crates have independent version lines and do not need to match@tauri-apps/api.
After running:
git add package.json bun.lock src-tauri/Cargo.toml src-tauri/Cargo.lock
git commit -m "chore: upgrade tauri to vX.Y"- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Follow the code style — Biome (
bun run lint:fix && bun run format:fix) - Commit with Conventional Commits
- Open a pull request
MIT — see LICENSE
- Issues: github.com/ragaeeb/libaby/issues
- Repository: github.com/ragaeeb/libaby
{ "books": [ { "id": 1681, "name": "صحيح البخاري", "author": { "id": 55, "name": "محمد بن إسماعيل البخاري" }, "category": { "id": 23, "name": "الحديث" }, "printed": 1, "version": "5.0", // NOTE: may be a string float — handled in Rust ... } ] }