G.STANCUTA
Publicat · 2026 · 05 · 128 min de citit

Aplicații Desktop fără Taxa Chromium: Rust, Tauri și Polysong

  • rust
  • tauri
  • desktop
  • sqlite

Electron livrează un browser complet cu fiecare aplicație. Tauri folosește webview-ul sistemului de operare și un nucleu Rust, reducând dimensiunea bundle-ului de 10 ori și memoria la jumătate. Iată ce costă cu adevărat acel compromis.

Îmi doream un manager local de bibliotecă muzicală. Ceva care scanează o structură de directoare, citește taguri audio, salvează totul în SQLite și nu atinge niciodată rețeaua. Polysong este acea aplicație. Am construit-o cu Rust și Tauri și după șase luni de utilizare zilnică am o imagine clară despre unde acest stack își merită reputația și unde îți pasează tăcut o problemă.

Arhitectura de Bază

Tauri împarte aplicația în două procese. Backend-ul este un binar Rust compilat cu logica de business. Frontend-ul este orice stack web dorești, randat în interiorul webview-ului nativ al sistemului de operare: WebKit pe macOS, WebView2 (bazat pe Chromium) pe Windows și WebKitGTK pe Linux. Cele două părți comunică printr-un pod de comenzi tipizat.

Webview-ul nu este livrat împreună cu aplicația. Utilizatorul îl are deja. Pe macOS și Windows modern vine cu sistemul de operare. Acest singur fapt explică de ce un installer Tauri poate cântări 4-8 MB în timp ce o aplicație Electron echivalentă cântărește 80-150 MB. Binarul Rust este compilat în avans, deci nu există nici un runtime Node.js.

Diagramă de arhitectură Tauri care arată nucleul Rust, webview-ul sistemului de operare și podul de comenzi
Tauri împarte munca clar: Rust deține I/O și datele, webview-ul sistemului de operare randează interfața.

Podul de Comenzi

Fiecare apel din frontend către Rust trece printr-o funcție #[tauri::command]. Argumentele sunt deserializate din JSON, funcția rulează și valoarea returnată este serializată înapoi. Tipurile sunt explicite pe ambele părți. Dacă denumești greșit un câmp sau transmiți o formă incorectă, primești o eroare la runtime în stratul Rust în loc de corupere silențioasă a datelor.

Aceasta este comanda principală pe care Polysong o folosește pentru a interoga biblioteca. Primește un șir de căutare și o cheie de sortare, accesează SQLite prin rusqlite și returnează un vector de înregistrări de piese.

rust
use serde::{Deserialize, Serialize};
use tauri::State;
use rusqlite::params;

use crate::db::DbConn;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TrackRecord {
    pub id: i64,
    pub title: String,
    pub artist: String,
    pub album: String,
    pub duration_secs: f64,
    pub file_path: String,
}

#[tauri::command]
pub async fn search_tracks(
    query: String,
    sort_by: Option<String>,
    db: State<'_, DbConn>,
) -> Result<Vec<TrackRecord>, String> {
    let conn = db.lock().map_err(|e| e.to_string())?;
    let sort_col = match sort_by.as_deref() {
        Some("artist") => "artist, title",
        Some("album")  => "album, track_num",
        _              => "title",
    };
    let pattern = format!("%{}%", query.to_lowercase());
    let sql = format!(
        "SELECT id, title, artist, album, duration_secs, file_path
         FROM tracks
         WHERE lower(title) LIKE ?1
            OR lower(artist) LIKE ?1
            OR lower(album) LIKE ?1
         ORDER BY {sort_col}
         LIMIT 500"
    );
    let mut stmt = conn.prepare(&sql).map_err(|e| e.to_string())?;
    let rows = stmt
        .query_map(params![pattern], |row| {
            Ok(TrackRecord {
                id:            row.get(0)?,
                title:         row.get(1)?,
                artist:        row.get(2)?,
                album:         row.get(3)?,
                duration_secs: row.get(4)?,
                file_path:     row.get(5)?,
            })
        })
        .map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

Pe partea de frontend, apelul este un singur invoke din @tauri-apps/api/core. Tipurile TypeScript sunt generate automat dacă folosești flag-ul plugin generate bindings al CLI-ului Tauri, sau le poți scrie manual pentru suprafețe mici.

ts
import { invoke } from '@tauri-apps/api/core';

interface TrackRecord {
  id: number;
  title: string;
  artist: string;
  album: string;
  duration_secs: number;
  file_path: string;
}

export async function searchTracks(
  query: string,
  sortBy?: 'title' | 'artist' | 'album',
): Promise<TrackRecord[]> {
  return invoke<TrackRecord[]>('search_tracks', {
    query,
    sort_by: sortBy ?? null,
  });
}

Configurare: tauri.conf.json

Majoritatea comportamentului Tauri este controlat prin tauri.conf.json. Pentru Polysong restricționez agresiv suprafața de capability: fără acces la shell din webview, acces la sistem de fișiere limitat la directorul bibliotecii muzicale, fără HTTP arbitrar din frontend. Acesta este unul dintre cele mai bune argumente pentru Tauri față de Electron din perspectiva securității. Postura implicită este deny-all; capability-urile se adaugă explicit.

json
{
  "productName": "Polysong",
  "version": "0.3.0",
  "identifier": "io.polysong.app",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:5173"
  },
  "app": {
    "windows": [
      {
        "title": "Polysong",
        "width": 1200,
        "height": 780,
        "minWidth": 800,
        "minHeight": 560,
        "decorations": true,
        "resizable": true
      }
    ],
    "security": {
      "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico"]
  }
}

Compromisul Real: Divergența Webview între Platforme

Aici Tauri câștigă asteriscul său. Pe macOS și Linux obții WebKit. Pe Windows obții WebView2, care este bazat pe Chromium. Nu sunt același motor și diferențele apar în comportamentul CSS și JavaScript, în special pentru audio, video și unele cazuri limită de layout.

Pentru Polysong m-a afectat în pipeline-ul audio. Web Audio API se comportă ușor diferit între WebKit și WebView2 pentru anumite dimensiuni de buffer. Am ajuns să mut redarea audio complet în stratul Rust folosind rodio și să expun o suprafață mică de comenzi pentru play, pause, seek și volum. Era oricum probabil decizia corectă, deoarece Rust îmi oferă redare fără pauze și ReplayGain fără acrobații suplimentare.

  • Testează pe toate trei platformele înainte de a te angaja la orice animație CSS sau cod Web Audio
  • Orice este sensibil la performanță sau specific platformei aparține Rust, nu webview-ului
  • CSS grid și flexbox sunt suficient de consistente pe toate trei platformele pentru a fi scrise o singură dată
  • Fonturile se randează diferit între WebKit și WebView2; folosește font-synthesis: none și servește propriile fonturi
  • WebView2 pe Windows necesită instalarea runtime-ului WebView2; Windows 11 modern îl include, sistemele mai vechi s-ar putea să nu îl aibă

Povestea memoriei este cu adevărat mai bună decât Electron. O fereastră Polysong proaspătă cu o bibliotecă de 40.000 de piese încărcată se situează în jurul a 90 MB RSS. Aplicația Electron echivalentă pe care am prototipat-o anterior a atins 250 MB făcând aceeași muncă. Cea mai mare parte a amprentei Tauri este randerul webview în sine; binarul Rust este neglijabil.

Rularea Acestui Stack cu un Agent AI de Programare

Lucrez regulat cu un agent AI de programare (Claude Code) la Polysong. Rust plus Tauri plus SQLite nu este o suprafață mică. Agentul are cunoștințe generale solide de Rust, dar Tauri 2.x are breaking changes față de 1.x, sistemul de capability este nou și convențiile mele de proiect sunt specifice. Fără context persistent agentul regresează: scrie sintaxa allowlist din Tauri 1.x, uită că audio trăiește în Rust, sugerează versiuni greșite de crate.

Soluția este un fișier AGENTS.md la rădăcina proiectului. Este inclus în git, mereu prezent și îl actualizez de fiecare dată când iau o decizie structurală. Agentul îl citește la începutul fiecărei sesiuni și operează cu imaginea completă actuală a proiectului.

Diagramă a unui agent AI care citește fișiere markdown de memorie pentru a menține contextul proiectului între sesiuni
Fișierele de context markdown persistente oferă agentului o memorie pe termen lung a convențiilor proiectului tău.

Iată AGENTS.md-ul real din Polysong, ușor prescurtat:

md
# Polysong – Agent Context

## Stack
- Rust (stable, 1.78+) + Tauri 2.x + SQLite via rusqlite 0.31
- Frontend: SvelteKit (static adapter, no SSR), TypeScript, Tailwind 4
- Audio playback: Rust only, via rodio 0.18. Do NOT use Web Audio API for playback.
- DB migrations: barrel-migrations crate, files in src-tauri/migrations/

## Tauri version note
This project uses **Tauri 2.x**. The old allowlist is GONE.
Permissions live in src-tauri/capabilities/. Each capability file is JSON.
Never write tauri::Builder allowlist config — it will not compile.

## Commands
- `cargo tauri dev` – dev mode with hot reload
- `cargo tauri build` – production bundle
- `cargo test` – Rust unit tests (run from src-tauri/)
- `pnpm dev` – frontend only (not connected to Tauri backend)

## Architecture decisions
- All file system access goes through Rust commands. The frontend never touches the FS directly.
- Audio: playback, seek, and volume are Rust commands. The frontend sends intents, gets state back.
- Search is SQLite FTS5 for albums/artists, LIKE for track titles (FTS5 overkill for title search).
- Window state (last size/position) is saved via tauri-plugin-window-state.

## Known gotchas
- WebKit (macOS/Linux) and WebView2 (Windows) differ on Web Audio. Keep audio in Rust.
- rusqlite must be compiled with the `bundled` feature on Windows or linking fails.
- Font rendering differs between platforms. Use `font-synthesis: none` and local fonts.
- The migrations barrel runner requires migration files numbered 001_, 002_ etc., no gaps.

## File layout
```
src-tauri/
  src/
    commands/   # Tauri command handlers
    db/         # Connection pool and query helpers
    audio/      # rodio playback engine
  migrations/   # SQL migration files
  capabilities/ # Tauri 2 permission manifests
src/            # SvelteKit frontend
```

Acel fișier a eliminat majoritatea regresiunilor de pierdere a contextului. Când deschid o nouă sesiune cu agentul, spun "citește mai întâi AGENTS.md" și agentul operează imediat în cadrul corect: versiunea Tauri corectă, versiunile de crate corecte, constrângerile arhitecturale corecte. Nu mai sugerează Web Audio pentru redare. Scrie manifeste de capability în loc de allowlist-uri. Calitatea primei încercări la orice sarcină crește semnificativ.

Merită Tauri

Pentru Polysong: da, clar. Bundle-ul este mic, memoria este redusă și mutarea muncii intensive de I/O în Rust este plăcută, nu dureroasă. Integrarea SQLite prin rusqlite este cea mai bună pe care am folosit-o vreodată în orice limbaj. Redarea audio fără pauze în rodio a durat o după-amiază.

Divergența webview între platforme este reală, dar gestionabilă dacă urmezi o regulă: orice atinge primitive ale sistemului de operare, audio, video, I/O de fișiere sau calcul sensibil la performanță merge în Rust. Webview-ul este o suprafață de randare. Menține-l astfel și povestea multiplă de platforme rezistă.

Dacă ai nevoie de conținut web bogat care trebuie să se comporte identic peste tot, Electron rămâne alegerea pragmatică. Dar dacă construiești o aplicație utilitară cu integrare reală de sistem, Tauri îți oferă un rezultat mai mic, mai rapid și mai defensibil. Curba de învățare Rust este costul sincer. Nu este superficială, dar se amortizează într-o bază de cod unde corectitudinea contează cu adevărat.

Tauri nu face dezvoltarea desktop mai ușoară. O face mai mică, mai rapidă și mai sinceră în privința locului unde aparține munca.

Portofoliu · Indicator
Desenat de
G. STANCUTA
Disciplină
AI & AUTOMATION
Locație
MORTER · SÜDTIROL
Stare
Disponibil
Limbi
IT · EN · RO · DE+
Stack
PLOI · HETZNER
Revizie
REV 2026.A
2026

© 2026 Gabriel Stancuta · jumpinotech.com — Proiectat cu AI, construit să funcționeze singur.