Turn any webpage into a desktop app with Rust
46.8k stars · 92.5% Rust · MIT License · v3.10
github.com/tw93/Pake
Explain Like I'm Five
You use websites every day — ChatGPT, YouTube, Twitter. But they live inside your browser, mixed in with dozens of tabs.
Pake wraps a website in a tiny native window. Think of it like putting a picture frame around a webpage — but the frame is built with Rust, so it's fast and tiny.
pake https://chat.openai.com --name ChatGPT
That's it. You get a real desktop app.
Your computer already has a built-in web viewer (called a WebView). Pake reuses it instead of shipping its own browser.
Anything with a URL. Pake ships popular pre-built apps and you can make your own.
| ChatGPT | Gemini |
| DeepSeek | Grok |
| Twitter / X | YouTube |
| YouTube Music | Excalidraw |
| WeRead | XiaoHongShu |
Pake turns websites into real desktop apps. It uses Rust and your OS's built-in web viewer to make apps that are 20x smaller and faster than Electron. One command is all it takes.
Architecture, code patterns, and internals
Pake is a hybrid Rust + TypeScript project. The Rust core is the app; the TypeScript CLI is the builder tool.
// Rust core (92.5% of codebase) src-tauri/ src/ main.rs // Entry point lib.rs // Tauri app builder util.rs // Utilities app/ config.rs // Config structs window.rs // WebView builder setup.rs // Tray & shortcuts invoke.rs // Tauri commands menu.rs // macOS menu bar inject/ style.js // CSS injection event.js // Keyboard & nav component.js // UI components auth.js // OAuth handling custom.js // User scripts theme_refresh.js
// TypeScript CLI (pake-cli) bin/ cli.ts // CLI entry point defaults.ts // Default config types.ts // Type definitions dev.ts // Dev mode builders/ // Build orchestration helpers/ // Utility helpers options/ // CLI arg parsing utils/ // General utils // Config files src-tauri/ tauri.conf.json // Main config tauri.macos.conf.json // macOS tauri.linux.conf.json // Linux tauri.windows.conf.json pake.json // Pake-specific Cargo.toml // Rust deps
Tauri is the foundation — a Rust framework for building desktop apps with web frontends. Pake uses Tauri 2.10.2.
| Aspect | Electron | Tauri |
|---|---|---|
| Runtime | Bundles Chromium | OS WebView |
| Language | JavaScript | Rust |
| Binary size | ~150-200 MB | ~5 MB |
| RAM usage | High | Low |
| Security | Full Node.js | Sandboxed |
From main() to a running window — every Pake app follows this flow.
// main.rs #![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] fn main() { app_lib::run() }
Everything about a Pake app is driven by two config files: pake.json and tauri.conf.json.
struct PakeConfig { windows: Vec<WindowConfig>, user_agent: PlatformSpecific<String>, system_tray: PlatformSpecific<bool>, multi_instance: bool, multi_window: bool, } struct WindowConfig { url: String, url_type: String, // "web"|"local" hide_title_bar: bool, fullscreen: bool, resizable: bool, dark_mode: bool, enable_wasm: bool, force_internal_navigation: bool, // ... 16 more fields }
struct PlatformSpecific<T> { macos: T, linux: T, windows: T, } impl<T> PlatformSpecific<T> { fn get(&self) -> &T { // Returns current platform's value #[cfg(target_os = "macos")] return &self.macos; // ... } }
Pake registers 8 plugins to provide native capabilities beyond basic web rendering.
| Plugin | Purpose | Key Use |
|---|---|---|
| window-state | Persist window position/size | Remember layout across restarts |
| oauth | OAuth authentication flows | Google/GitHub login popups |
| http | Native HTTP requests | File downloads, API calls |
| shell | Open URLs in system browser | External link handling |
| notification | OS-level notifications | Download complete alerts |
| opener | Open files/folders natively | Downloaded file access |
| single-instance | Prevent duplicate windows | Conditional on config |
| global-shortcut | System-wide hotkeys | Activation shortcut with debounce |
The window.rs module orchestrates WebView creation with platform-specific customization.
fn build_window( app: &AppHandle, opts: WindowBuildOptions ) { // 1. Resolve URL type match url_type { "web" => WebviewUrl::App(url), "local" => WebviewUrl::App(path), } // 2. Configure user agent builder.user_agent(config.user_agent.get()); // 3. Inject JS scripts (6 files) builder.initialization_script(...); // 4. Platform-specific setup #[cfg(target_os = "macos")] builder.title_bar_style(...); #[cfg(target_os = "windows")] builder.browser_args(proxy, flags); }
// Browser args include: "--disable-blink-features= AutomationControlled"
Prevents websites from detecting the app as an automated browser.
Pake injects 6 JavaScript files into every WebView on page load, providing the "native app" experience.
The event injection handles keyboard shortcuts, link navigation, context menus, downloads, and paste interception.
| Key | Action |
|---|---|
| Cmd/Ctrl + [ | Navigate back |
| Cmd/Ctrl + ] | Navigate forward |
| Cmd/Ctrl + - | Zoom out |
| Cmd/Ctrl + = | Zoom in |
| Cmd/Ctrl + 0 | Reset zoom |
| Cmd/Ctrl + R | Reload page |
| Cmd/Ctrl + arrows | Scroll top/bottom |
detectAnchorElementClick(e) { // Classify the link if (isInternal(href)) return; // navigate normally if (isDownloadableFile(href)) invoke('download_file', href); if (target === '_blank') shell.open(href); // system browser } // window.open is overridden too window.open = (url) => { if (isAuthPopup(url)) return original(url); shell.open(url); }
On DOMContentLoaded, Pake injects CSS to transform web pages into "app-like" experiences.
When hide_title_bar is enabled on macOS, a second stylesheet adds 20-60px top padding to headers and navs so content doesn't hide behind the native traffic lights.
// On DOMContentLoaded: const style = document .createElement('style'); style.textContent = ` /* 400+ lines of CSS */ .twitter-header { display: none; } .chatgpt-sidebar { width: 240px; } /* ... */ `; document.head.appendChild(style); // Conditional title-bar CSS if (pakeConfig?.hide_title_bar && navigator.platform === 'MacIntel') { // Inject padding overrides }
Pake exposes 5 Rust commands callable from the WebView JavaScript via Tauri's IPC bridge.
| Command | What It Does | Called From |
|---|---|---|
| download_file | Streams a URL to ~/Downloads with progress toasts | Link clicks, context menu |
| download_file_by_binary | Writes binary data (blob/data URIs) to disk | Blob URL handling in event.js |
| send_notification | Shows OS notification with title, body, icon | Download completion |
| update_theme_mode | Switches dark/light mode (macOS only) | Theme refresh script |
| clear_cache_and_restart | Wipes WebView data and relaunches | User-triggered (tray menu) |
The setup.rs module initializes two key native features.
// Tray click handler if window.is_visible() { window.minimize(); } else { window.show(); window.set_focus(); }
The TypeScript CLI is how users actually build Pake apps. It orchestrates the entire build pipeline.
# Install globally pnpm install -g pake-cli # Basic usage pake https://github.com --name GitHub # With options pake https://chat.openai.com \ --name ChatGPT \ --icon ./icon.png \ --width 1200 \ --height 780 \ --hide-title-bar \ --dark-mode \ --system-tray
The defaults.ts file defines sensible defaults for every option.
| Width | 1200px |
| Height | 780px |
| Resizable | true |
| Fullscreen | false |
| Hide title bar | false |
| Always on top | false |
| Zoom | 100% |
| Dark mode | false |
| Multi-instance | false |
| Drag & drop | false |
| WASM | false |
| System tray | false |
| Incognito | false |
| Camera/Mic | false |
| Hide on close | macOS: true |
| Ignore certs | false |
Platform targets: .deb/.appimage (Linux), .dmg (macOS), .msi (Windows)
Pake achieves its ~5MB binary through aggressive Rust release profile tuning.
[profile.release] panic = "abort" # No unwinding lto = "thin" # Link-Time Opt strip = true # Remove symbols
Electron (Slack)
Electron (minimal)
Pake app
The Cargo.toml reveals a focused dependency tree centered on Tauri 2.x.
| Crate | Version | Role |
|---|---|---|
| tauri | 2.10.2 | Application framework |
| tokio | 1.49.0 | Async runtime (full) |
| serde | 1.0.228 | Serialization |
| serde_json | 1.0.149 | JSON parsing |
[features] # Development builds cli-build = [] # Production builds (default) custom-protocol = [ "tauri/custom-protocol" ] default = ["custom-protocol"]
Pake uses Rust's conditional compilation extensively to handle OS differences.
#[cfg(target_os = "macos")] builder.title_bar_style( TitleBarStyle::Overlay );
#[cfg(target_os = "linux")] // Disable DMABUF to prevent rendering // issues on certain compositors std::env::set_var( "WEBKIT_DISABLE_DMABUF_RENDERER", "1" );
Pake bridges the WebView sandbox to the filesystem for file downloads through two complementary paths.
#[tauri::command] async fn download_file( url: String, filename: String, lang: Option<String> ) -> Result<(), String> { // 1. Resolve ~/Downloads path // 2. Stream response in chunks // 3. Write to disk // 4. Show progress toast // 5. Send notification on complete }
// event.js handles blob: and data: URIs if (url.startsWith('blob:')) { const reader = new FileReader(); reader.readAsArrayBuffer(blob); reader.onload = () => invoke('download_file_by_binary', { binary: Array.from( new Uint8Array(result) ) }); } if (url.startsWith('data:')) { // Decode base64 → binary // Send to Rust via IPC }
Tauri 2.x's capability-based security model constrains what each window can access.
46.8k stars · Rust + Tauri 2.x · MIT License
Try it: pnpm install -g pake-cli && pake https://yourapp.com