Bare er en eksperimentell markdown-nettleser med fokus på personvern, hastighet og rent innhold. Nettleseren ignorerer tradisjonelle nettsider og rendrer kun .md-filer direkte fra HTTP-responser.
- Prosjektet er lisensiert under GNU General Public License v3.0 (GPL-3.0).
- Bidrag til repoet skal kompatible med GPL-3.0.
- Enkelhet: Minimal funksjonalitet, ingen unødvendig kompleksitet
- Personvern: Null sporing, ingen cookies, ingen JavaScript-kjøring
- Hastighet: Lav båndbredde, raske lastetider, effektiv rendering
- Fokus: Innholdet er i sentrum, ikke designet
| Komponent | Teknologi | Versjon |
|---|---|---|
| App-rammeverk | Tauri | 2.0+ |
| Backend | Rust | Latest stable |
| Markdown parser | pulldown-cmark | Latest |
| HTTP-klient | reqwest | Latest |
| HTML→MD | html2md | Latest |
| Frontend | Vanilla HTML/CSS/JS | - |
Stil:
// Bruk idiomatisk Rust
// Foretrekk Result<T, E> over panics
// Bruk ? for error propagation
// Dokumenter alle public APIs med ///
/// Henter en markdown-fil fra en URL
///
/// # Arguments
/// * `url` - URL til markdown-filen
///
/// # Returns
/// * `Ok(String)` - Innholdet i markdown-filen
/// * `Err(FetchError)` - Hvis nedlasting feiler
pub async fn fetch_markdown(url: &str) -> Result<String, FetchError> {
let response = reqwest::get(url).await?;
let content = response.text().await?;
Ok(content)
}Prinsipper:
- Bruk
async/awaitfor I/O-operasjoner - Unngå
unwrap()i produksjonskode, bruk?eller.ok()+ håndtering - Foretrekk
&stroverStringfor parametere når mulig - Bruk
ResultogOptioneksplisitt, ikke implisitt - Skriv tester for all kjernelogikk
Kritiske regler:
- ❌ ALDRI implementer cookie-støtte
- ❌ ALDRI implementer JavaScript-kjøring i WebView
- ❌ ALDRI send brukerdata til eksterne tjenester uten eksplisitt samtykke
- ✅ Alltid sanitize HTML før konvertering til markdown
- ✅ Alltid valider URLer før HTTP-forespørsler
- ✅ Bruk HTTPS som standard, advare ved HTTP
Eksempel på URL-validering:
fn validate_url(url: &str) -> Result<Url, ValidationError> {
let parsed = Url::parse(url)?;
// Kun HTTP/HTTPS
match parsed.scheme() {
"http" | "https" => Ok(parsed),
_ => Err(ValidationError::UnsupportedScheme),
}
}Prinsipper:
- Bruk pulldown-cmark for all markdown-parsing
- Støtt CommonMark + GitHub Flavored Markdown (tabeller, task lists)
- Konverter markdown til ren HTML for visning i WebView
- Ingen inline JavaScript i generert HTML
- Minimal CSS, fokus på lesbarhet
Eksempel:
use pulldown_cmark::{Parser, Options, html};
fn render_markdown(content: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
let parser = Parser::new_ext(content, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}Retningslinjer:
- Bruk html2md for konvertering
- Fjern scripts, style tags, og tracking-elementer før konvertering
- Bevar semantisk struktur (headings, lists, links)
- Håndter bilder basert på brukerprefereranser
Eksempel:
fn convert_html_to_markdown(html: &str) -> String {
// Sanitize først
let clean_html = sanitize_html(html);
// Konverter
html2md::parse_html(&clean_html)
}
fn sanitize_html(html: &str) -> String {
// Fjern <script>, <style>, tracking pixels, etc.
// Implementer med ammonia eller lignende
todo!()
}Nåværende implementasjon (Fase 1):
- Bilder vises inline som standard
- Ingen spesiell håndtering - bruker standard HTML
<img>tags
Fremtidig arkitektur (planlagt):
/// Brukerpreferanse for bildehåndtering
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ImageMode {
/// Vis bilder inline (standard)
Show,
/// Skjul alle bilder
Hide,
/// Vis placeholder med alt-tekst, klikk for å laste
Placeholder,
}
/// Konfigurasjon for bildevisning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageSettings {
/// Global innstilling
pub global_mode: ImageMode,
/// Per-side overrides (URL -> modus)
pub site_overrides: HashMap<String, ImageMode>,
}UI-plan for brukervalg:
- Toolbar-knapp for å toggle bildmodus på gjeldende side
- Innstillinger-panel for global preferanse
- Lagres i
~/.config/bare/settings.json(eller tilsvarende per OS)
Prosjektet bruker GitHub Actions for automatisert bygg og testing.
Workflows:
| Fil | Trigger | Beskrivelse |
|---|---|---|
.github/workflows/ci.yml |
Push/PR til main | Tester og linting |
.github/workflows/build.yml |
Release tags | Bygg for alle plattformer |
Dette MÅ du sjekke hver gang du endrer koden, fiks eventuelle feil:
# Formatering
cargo fmt --check
# Linting
cargo clippy -- -D warnings
# Tester
cargo test
# Bygg-verifisering
cargo tauri buildNår du legger til ny funksjonalitet:
- Skriv tester som kjører i CI
- Alle tester kjører uten feil
- Sørg for at
cargo clippypasserer uten warnings - Formater kode med
cargo fmt - cargo build --release bygger uten feilmeldinger
bare/
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Tauri entrypoint
│ │ ├── lib.rs # Library root
│ │ ├── commands.rs # Tauri IPC commands
│ │ ├── markdown.rs # Markdown rendering
│ │ ├── fetcher.rs # HTTP client logic
│ │ ├── converter.rs # HTML→MD conversion
│ │ ├── settings.rs # Settings management
│ │ ├── bookmarks.rs # Bookmarks management
│ │ ├── gemini.rs # Gemini protocol client
│ │ ├── gemtext.rs # Gemtext→Markdown converter
│ │ ├── gopher.rs # Gopher protocol client (RFC 1436)
│ │ └── gophermap.rs # Gophermap→Markdown converter
│ ├── Cargo.toml
│ └── tauri.conf.json
├── src/
│ ├── index.html # Main UI
│ ├── styles.css # Minimal styling
│ └── js/ # Modularisert frontend
│ ├── constants.js # Konstanter og konfigurasjoner
│ ├── state.js # State management (history, bookmarks, settings)
│ ├── dom.js # DOM-elementer og utilities
│ ├── ui.js # UI-funksjoner (status, panels, dialogs)
│ ├── settings.js # Innstillingshåndtering
│ ├── bookmarks.js # Bokmerkehåndtering
│ ├── search.js # Søkefunksjonalitet
│ ├── navigation.js # Navigasjon og URL-håndtering
│ ├── events.js # Event listeners og shortcuts
│ └── main.js # Entry point og initialisering
├── .github/
│ └── copilot-instructions.md
├── README.md
├── PLAN.md
└── LICENSE
Definisjon:
#[tauri::command]
async fn fetch_and_render(url: String) -> Result<RenderedPage, String> {
let content = fetch_markdown(&url)
.await
.map_err(|e| e.to_string())?;
let html = render_markdown(&content);
Ok(RenderedPage { html, title: extract_title(&content) })
}Bruk fra frontend:
import { invoke } from '@tauri-apps/api/core';
async function loadPage(url) {
try {
const result = await invoke('fetch_and_render', { url });
displayContent(result.html);
} catch (error) {
showError(error);
}
}I tauri.conf.json:
{
"security": {
"csp": "default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:",
"dangerousDisableAssetCspModification": false
},
"allowlist": {
"all": false,
"http": {
"all": false,
"request": true,
"scope": ["http://**", "https://**"]
}
}
}- Vanilla JavaScript, ingen frameworks
- Modularisert arkitektur med klare avhengigheter
- Minimal CSS, fokus på typografi og lesbarhet
- Keyboard-first navigation
- Responsive design
Moduler lastes i avhengighetsrekkefølge:
constants.js- Konstanter (ingen avhengigheter)state.js- State management (bruker konstanter)dom.js- DOM-elementer og utilitiesui.js- UI-funksjoner (bruker state, dom, constants)settings.js- Innstillinger (bruker state, ui)bookmarks.js- Bokmerker (bruker state, ui)search.js- Søk (bruker state, dom)navigation.js- Navigasjon (bruker alt over)events.js- Event handlers (bruker alt over)main.js- Entry point (orkestrerer alt)
Designprinsipper:
- Ingen globale klasser, kun funksjoner og globale variabler
- Klare navnekonvensjoner (f.eks.
elements.btnMenu,toggleDropdownMenu()) - Separasjon av concerns (UI, state, navigation, events)
Toolbar:
- URL-bar med Enter-to-submit
- Navigasjonsknapper (back, forward, home)
- Bokmerkeknapper (add, view list)
- Åpne fil-knapp
- Innstillinger-knapp
- 3-prikks meny med mindre brukte funksjoner:
- Zoom-kontroller (−/+)
- Bytt tema
- Om-dialog
Side-paneler:
- Bokmerker-panel (høyre side)
- Innstillinger-panel (høyre side)
- Kan kun ha ett panel åpent om gangen
Modaler:
- Om-dialog med versjonsinformasjon og app-beskrivelse
- Overlay med klikk-utenfor-for-å-lukke
Søkebar:
- Toggle med Ctrl+F
- Inline highlight av søkeresultater
- Navigasjon mellom treff
Markdown Viewport:
- Ingen custom scrollbars (bruk native)
- Systemfonter for optimal lesbarhet
- Lys/mørk modus basert på system-preferanser
| Shortcut | Handling |
|---|---|
Ctrl+O |
Åpne fil |
Ctrl+F |
Søk i side |
Ctrl+D |
Toggle bokmerke |
Ctrl+B |
Vis bokmerker |
Ctrl++ |
Zoom inn |
Ctrl+- |
Zoom ut |
Ctrl+0 |
Tilbakestill zoom |
Ctrl+L |
Fokuser URL-bar |
Alt+← |
Tilbake |
Alt+→ |
Fremover |
Escape |
Lukk paneler/dialogs/søk |
g |
Gå hjem (ikke i input) |
j/k |
Scroll ned/opp (Vim-stil) |
G |
Scroll til bunnen |
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markdown_rendering() {
let input = "# Hello\n\nThis is **bold**.";
let output = render_markdown(input);
assert!(output.contains("<h1>Hello</h1>"));
assert!(output.contains("<strong>bold</strong>"));
}
#[tokio::test]
async fn test_fetch_markdown() {
// Mock HTTP client eller bruk wiremock
}
}- Test komplett flyt: URL input → fetch → render → display
- Mock eksterne HTTP-kall
- Test feilhåndtering (404, timeout, invalid URL)
Definér custom error types:
#[derive(Debug, thiserror::Error)]
pub enum BareError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Conversion error: {0}")]
Conversion(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}Bruk i hele kodebasen:
pub type BareResult<T> = Result<T, BareError>;
pub async fn process_url(url: &str) -> BareResult<String> {
let validated = validate_url(url)?;
let content = fetch_content(&validated).await?;
let markdown = convert_if_needed(content)?;
Ok(markdown)
}- Cache konverterte sider i minnet (LRU cache)
- Stream store filer i stedet for å laste alt i minnet
- Bruk async/await for ikke-blokkerende I/O
- Lazy-load bilder hvis støtte legges til
- Begrens history stack (f.eks. siste 50 sider)
- Rydd opp i cache regelmessig
- Unngå minnelekkasjer i WebView
- Semantisk HTML i rendret markdown
- Keyboard navigation (Tab, Shift+Tab, Enter)
- Screen reader-vennlig
- Høy kontrast i mørk/lys modus
- Skalerbar tekst
Logging:
use log::{info, warn, error, debug};
pub async fn fetch_content(url: &Url) -> BareResult<String> {
info!("Fetching content from: {}", url);
let response = reqwest::get(url.as_str()).await?;
let status = response.status();
if !status.is_success() {
warn!("Non-success status: {} for {}", status, url);
}
let content = response.text().await?;
debug!("Fetched {} bytes", content.len());
Ok(content)
}Tauri DevTools:
- Bruk
tauri devfor hot-reload - Åpne DevTools i WebView for frontend-debugging
- Bruk
dbg!()macro for quick Rust-debugging
Når du foreslår kode:
- Følg eksisterende kodestruktur
- Skriv tester for ny funksjonalitet
- Dokumenter public APIs
- Vurder personvern og sikkerhet først
- Hold det enkelt - ikke legg til unødvendig kompleksitet
- ❌ En fullverdig nettleser (vi vil aldri støtte JavaScript)
- ❌ En HTML-renderer (kun markdown er førsteklasses)
- ❌ Et sosiale medie-verktøy (ingen commenting, sharing, etc.)
- ❌ En tekstbehandler (kun visning, ikke redigering)
- ✅ Gemini-protokoll støtte (gemini://) - IMPLEMENTERT i v0.1.3
- ✅ Gopher-protokoll støtte (gopher://) - IMPLEMENTERT i v0.1.4
- ✅ Lokale markdown-filer (file://)
⚠️ Eksport til PDF⚠️ Custom themes/CSS⚠️ Tab-støtte (vurder kompleksitet vs. nytte)⚠️ Synkronisering (kun hvis lokalt, ikke cloud)
Implementasjon (v0.1.3):
Bare støtter nå fullstendig Gemini-protokollen med følgende arkitektur:
gemini.rs:
// Hovedstrukturer
pub struct GeminiClient {
tls_config: Arc<ClientConfig>,
tofu_store: Arc<Mutex<TofuStore>>,
}
pub struct GeminiResponse {
pub status: u8,
pub meta: String,
pub body: String,
pub final_url: String,
}Nøkkelprinsipper:
- TOFU (Trust On First Use) for TLS-sertifikater
- SHA-256 fingerprint-lagring i
~/.config/bare/known_hosts.json - Iterativ redirect-håndtering (maks 5 redirects)
- Alle statuskoder håndteres: 10-11 (input), 20 (success), 30-31 (redirect), 40-69 (errors)
- Timeout: 10 sekunder
- Maksimal respons: 5 MB
- Custom TofuVerifier som implementerer ServerCertVerifier
gemtext.rs:
pub fn gemtext_to_markdown(input: &str) -> GemtextResult {
// Linje-basert parsing
// => url text → [text](url)
// * item → - item
// ### heading → ### heading
// ``` preformatted blocks → ``` code blocks
}Konverteringsregler:
- Link-linjer:
=> gemini://example.com/page Example→[Example](gemini://example.com/page) - List-items:
* Item→- Item - Headings: Direkte pass-through (
#,##,###) - Preformatted:
```alt-text→```alt-text(bevares) - Blockquotes:
> text→> text(bevares)
navigation.js:
async function loadGeminiUrl(url, addHistory = true) {
const result = await invoke('fetch_gemini', { url, window: appWindow });
if (result.startsWith('GEMINI_INPUT_PROMPT:')) {
// Vis input-dialog
showGeminiInputDialog(prompt, url, sensitive);
} else {
// Render innhold
displayContent(result);
}
}Gemini Input Dialog:
- Modal overlay med prompt-tekst fra server
- Støtte for sensitive input (status 11 = passord-type)
- Enter-to-submit, Escape-to-cancel
- Automatisk fokus på input-felt
TOFU-implementasjon:
- Første besøk: Lagre sertifikat-fingerprint
- Påfølgende besøk: Verifiser mot lagret fingerprint
- Endring: Vis feilmelding til bruker (mulig MITM-angrep)
- Ikke CA-basert PKI (i tråd med Gemini-filosofien)
Validering:
fn validate_gemini_url(url: &str) -> Result<Url, GeminiError> {
let parsed = Url::parse(url)?;
if parsed.scheme() != "gemini" {
return Err(GeminiError::InvalidUrl("Not a gemini URL".into()));
}
if url.len() > MAX_URL_LENGTH {
return Err(GeminiError::InvalidUrl("URL too long".into()));
}
Ok(parsed)
}Unit tests:
- gemini.rs: 19 tests (URL-validering, response-parsing, TOFU-logikk, redirect-håndtering)
- gemtext.rs: 17 tests (alle gemtext-elementer, edge cases)
- fetcher.rs: 4 nye tests (gemini:// URL-håndtering)
Manuelle test-scenarier:
- Naviger til
gemini://geminiprotocol.net/ - Klikk på lenker innenfor Gemini-space
- Test input-forespørsler (status 10)
- Verifiser TOFU-lagring i
~/.config/bare/known_hosts.json - Test cross-protokoll navigasjon (gemini → http og omvendt)
- Test back/forward mellom protokoller
#[derive(Debug, thiserror::Error)]
pub enum GeminiError {
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("TLS error: {0}")]
TlsError(String),
#[error("Certificate changed for {0} - possible MITM attack")]
CertificateChanged(String),
#[error("Input required: {0}")]
InputRequired(String),
#[error("Redirect loop detected")]
RedirectLoop,
// ... flere error-typer
}Brukervendte feilmeldinger:
- Norsk språk i UI
- Tydelige forklaringer på TOFU-brudd
- Valgfrie handlinger (f.eks. "Gå tilbake" vs "Fortsett likevel")
Implementasjon (v0.1.4):
Bare støtter Gopher-protokollen (RFC 1436) med følgende arkitektur:
gopher.rs:
pub struct GopherResponse {
pub content_type: GopherContentType,
pub items: Vec<GopherItem>, // for gophermap
pub text: String, // for tekstfiler
pub final_url: String,
}
pub enum GopherContentType {
Menu,
Text,
}Nøkkelprinsipper:
- Asynkron TCP-tilkobling med tokio (ingen TLS nødvendig for standard Gopher)
- Parsing av alle elementtyper (0-9, i, h, s, +)
- Støtte for søk (type 7): tab-separert selektor + søkestreng
- Timeout: 10 sekunder
- Maksimal respons: 5 MB
gophermap.rs:
pub fn to_markdown(items: &[GopherItem], base_url: &str) -> GophermapResult {
// Konverterer Gopher-meny til Markdown
// Emoji-ikoner per type: 📁 mappe, 📄 tekst, 🔍 søk, osv.
// Automatisk tittel-ekstraksjon fra info-linjer
}navigation.js:
async function loadGopherUrl(url, addHistory = true) {
const result = await invoke('fetch_gopher', { url });
displayContent(result.html);
}Søkedialog (type 7):
- Modal overlay med søke-input
- Enter-to-submit, Escape-to-cancel
- Resultat lastes som en ny Gopher-side
- Naviger til
gopher://gopher.floodgap.com/ - Klikk på lenker i gophermap
- Test tekstfil-visning (type 0)
- Test søk på en søke-side (type 7)
- Verifiser cross-protokoll navigasjon (gopher → http og omvendt)
- Test back/forward mellom protokoller
# Utvikle
cargo tauri dev
# Bygg for produksjon
cargo tauri build
# Run Rust tests
cargo test
# Format kode
cargo fmt
# Linting
cargo clippy
# Sjekk for sikkerhetssårbarheter
cargo auditHusk: Når du jobber på Bare, prioriter alltid personvern og enkelhet over funksjonalitet. Hvis et feature krever kompromiss på kjerneverdiene, så er det ikke riktig for Bare.