Building a Cross-Platform File Search App With Tauri — Not Electron
Source: Dev.to
The Problem
Every knowledge worker I know has the same issue: files are scattered across Google Drive, Dropbox, SharePoint, Slack, Notion, GitHub, and the local machine.
When you need to find something you end up opening four different search bars.
The Solution – OmniFile
OmniFile is a single, global‑shortcut‑triggered search bar that finds files across all those sources instantly.
- Desktop app
- Privacy‑first – everything stays on your machine
I built it with Tauri + Rust instead of Electron, and integrated seven OAuth providers directly into the desktop app.
Why Tauri + Rust?
| Tauri (Rust) | Electron (Chromium) | |
|---|---|---|
| Installer size | ~8 MB | ~80 MB+ |
| Idle RAM usage | ~30 MB | ~150 MB+ |
| Backend language | Rust (fast, safe) | JavaScript |
| CPU‑intensive tasks | Rust excels at indexing & file I/O | Slower |
The trade‑off is writing the backend in Rust instead of JavaScript, but for a file‑search app that’s a benefit – Rust’s performance for walking directories and parsing file formats is hard to beat.
Search Engine – Tantivy
Tantivy is Rust’s answer to Lucene. I use it as the local search engine that indexes everything into a single queryable index.
schema_builder.add_text_field("title", TEXT | STORED); // Tokenized + returned
schema_builder.add_text_field("path", STRING | STORED); // Exact match
schema_builder.add_text_field("content", TEXT); // Searchable but NOT stored
schema_builder.add_text_field("source", STRING | STORED); // "local", "gdrive", etc.
schema_builder.add_i64_field("modified_at", INDEXED | STORED);
Key decision – store only metadata
- Content is indexed but not stored.
- The original file already lives on disk, so we re‑extract its content when we need to display it.
- This keeps the index small while still enabling full‑text search.
Provider‑Specific Indexing
Each cloud provider writes into the same Tantivy index, distinguished by a source tag.
let source_term = Term::from_field_text(source_field, "gdrive");
writer.delete_term(source_term); // Clear only gdrive docs
// …re‑index gdrive files…
writer.commit()?;
Delete‑and‑re‑index per provider means independent updates without touching other sources.
File Content Extraction
| Format | Extraction steps |
|---|---|
| DOCX | 1. Open the ZIP archive |
- Locate
word/document.xml - Parse XML and pull text from “ tags | | XLSX | 1. Open the ZIP archive
- Read the shared‑strings table
- Resolve cell indices to actual strings | | Plain text | Try UTF‑8 → fall back to Shift‑JIS (common for Japanese) → finally lossy UTF‑8 |
OAuth in a Desktop App
Desktop apps can’t receive OAuth callbacks via a public URL.
Solution: spin up a temporary local HTTP server for each OAuth flow.
Port mapping per provider
| Provider | Local port |
|---|---|
| Google Drive | 14200 |
| Dropbox | 14201 |
| Box | 14202 |
| SharePoint | 14203 |
| Slack (HTTPS) | 14204 |
| Notion | 14205 |
| GitHub | 14206 |
Flow:
- Open the system browser.
- User logs in.
- Provider redirects to
http://localhost:PORT/callback?code=XXX. - Local server captures the code and exchanges it for a token.
All providers use PKCE (Proof Key for Code Exchange) to protect against authorization‑code interception – especially important for desktop apps.
Slack’s HTTPS requirement
Slack only accepts https://localhost redirects.
I generate a self‑signed TLS certificate on‑the‑fly with rcgen and serve the callback over HTTPS:
let subject_alt_names = vec!["localhost".to_string(), "127.0.0.1".to_string()];
let CertifiedKey { cert, key_pair } = generate_simple_self_signed(subject_alt_names)?;
let config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(vec![cert_der], key_der)?;
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
The browser shows a certificate warning; the user clicks through, and the flow completes.
A retry loop handles the initial TLS handshake failure:
let mut tls_stream = match tls_acceptor.accept(stream).await {
Ok(stream) => stream,
Err(_) => continue, // Browser cert warning – wait for retry
};
Important: Bind the TCP listener before opening the browser to avoid a race condition where the redirect arrives before the server is ready.
GitHub Integration
GitHub’s Search API is limited (not all files indexed, stale data).
Instead I use the Trees API to fetch the entire repository file tree in one recursive call:
GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1
- Cache results for 5 minutes.
- Perform case‑insensitive matching client‑side.
- Batch tree fetches (10 repos at a time) with
futures::future::join_allto stay within rate limits.
If a cache refresh fails, the app gracefully falls back to the stale cache – better to show slightly outdated results than nothing.
Ranking Search Results
- Exact filename match (highest)
- Partial filename match
- Shorter path depth (higher‑level files are considered more relevant)
Bringing It All Together
OmniFile is designed to pop up instantly when you hit a keyboard shortcut (like Spotlight or Alfred).
Tauri’s lightweight native webview combined with a Rust backend gives us:
- Fast launch (global shortcut)
- Low memory footprint
- Privacy‑first architecture (all data stays on the user’s machine)
The result is a single, unified search experience across every place a knowledge worker stores files.
Global Shortcut Enhancements
- Debouncing – Prevents rapid show/hide cycles when the shortcut key is held down. A 300 ms threshold stops multiple triggers.
- Rollback on failure – If registering a new shortcut fails (e.g., the key is already claimed), the system automatically re‑registers the previous shortcut so the user never loses the ability to summon the app.
- Three‑level persistence –
- Stored in memory for fast access.
- Persisted to a settings file so it survives restarts.
- Reflected in the tray‑menu label for user visibility.
iCloud Drive Search (No Public API)
Apple doesn’t expose a public API for iCloud Drive search. The solution is simple:
-
Detect the iCloud Drive folder at
~/Library/Mobile Documents/com~apple~CloudDocs -
Treat it as a regular local directory.
-
Use a file watcher to pick up changes, index the contents with Tantivy, and provide search results.
This works as long as iCloud Drive syncs files to disk (the default behavior on macOS). No OAuth flow or API integration is required.
Current To‑Do List
- Unify OAuth config structs – I have seven separate
*OAuthConfigstructs that are ~90 % identical. A trait‑based approach would eliminate duplication. - Use Tantivy’s query parser for scoring – The current implementation iterates all documents with substring matching. Switching to Tantivy’s built‑in BM25 scoring will be more sophisticated and faster for large indexes.
- Plan for token refresh from day one –
- Some providers (Slack, Notion, GitHub) issue non‑expiring tokens.
- Others (Google, Microsoft) require refresh‑token flows.
This divergence currently creates special cases throughout the codebase.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Tauri 2 (Rust backend + native WebView) |
| Frontend | React 19 + TypeScript + Tailwind CSS |
| Search Engine | Tantivy (Rust, full‑text search) |
| OAuth | oauth2 crate + PKCE |
| TLS | rustls + rcgen (for Slack) |
| File Watching | notify crate with debouncer |
| File Parsing | quick-xml (DOCX/XLSX), encoding_rs (Shift‑JIS) |
omnifile.app
If you’re tired of searching five different places for one file, try
omnifile.app.
- Free tier – Local search only.
- Pro tier – $129 (lifetime) unlocks all cloud integrations.
Community Questions
- Which cloud integrations matter most to you?
- Would you use a CLI version alongside the GUI?
- Any clever search UX ideas?
Drop a comment or find me on GitHub.
Project Highlights
- Built as a solo project with Tauri + Rust.
- The entire app idles at ~30 MB RAM.
- Every search query stays on your machine — no server, no telemetry.