git.lucas.co / cce-browser
web browser (Servo)
git clone https://git.lucas.co/cce-browser.git

commitdc78c615a8bdea4d9ac8def01d48959fb7b9919a
parent0f265bcef0
authorLucas Galante <[email protected]>
date2026-08-31 08:20
feat: WPE WebKit is the default engine; Servo moves behind a feature

Forced by an incident, not preference drift. Two days after the port
landed, a routine featureless rebuild by another session overwrote the
installed WebKit browser with a fresh Servo one — silently, correctly,
doing exactly what the defaults said. The user found out when asked
whether cce-browser was "still using servo": it was again, with their
WebKit-stored logins invisible and the interstitial memory leak live. An
opt-in engine cannot survive a multi-session workspace, because defaults
are what other sessions build.

So: default = ["wpe"], and the Servo backend moves behind a `servo`
feature with its whole dependency tree (servo, dpi, euclid, rustls,
reqwest, http) optional. The split was clean — every Servo-only dep sat in
exactly one file. webview.rs, the ProtocolHandler impl in pages.rs, the
reqwest download worker, and the Theme conversion are gated; dom_key and
dom_button, dead in main.rs since the swap moved input mapping into the
hosts, are deleted rather than gated. Building with neither feature is a
compile_error naming both options.

Consequences worth the line each: a default build no longer compiles Servo
at all — seconds instead of minutes, 14 MB instead of 177 — which retires
WORKSPACE.md's reason for leaving this crate out of cce-ui sweeps; and
cargo test now actually builds the wpe examples by default, which exposed
that five of them were missing the EditingCommand shim and had only ever
compiled because their required-features were previously unmet. That is
the quiet cost of gating: code that is never built is never checked.

All three configurations verified: default (wpe), --no-default-features
--features servo, and both together (the sweep case). Twelve tests pass.
Installed; the Servo fallback binary and its README remain at
~/.local/state/cce/browser/.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md             | 33 +++++++++++++++++++++------------
 Cargo.toml            | 23 +++++++++++++++--------
 WPE-PORT.md           | 16 ++++++++++------
 examples/wpe_dark.rs  |  4 ++++
 examples/wpe_host.rs  |  4 ++++
 examples/wpe_input.rs |  4 ++++
 examples/wpe_loop.rs  |  4 ++++
 examples/wpe_tabs.rs  |  4 ++++
 src/downloads.rs      |  5 ++++-
 src/main.rs           | 46 +++++++++-------------------------------------
 src/pages.rs          |  4 ++++
 src/settings.rs       |  1 +
 12 files changed, 84 insertions(+), 64 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 746585e..4f7599e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,8 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 ## What this is
 
-`cce-browser` is a web browser for the CCE Wayland desktop environment, built on an
-**embedded, in-process Servo**. It is one crate of the multi-repo `cce` workspace (its
+`cce-browser` is a web browser for the CCE Wayland desktop environment, built on
+**embedded WPE WebKit** (since 2026-08-30; the original Servo backend survives behind
+a feature flag — see WPE-PORT.md for the whole port). It is one crate of the multi-repo `cce` workspace (its
 own git repo side-by-side with its siblings; published read-only at
 `https://git.lucas.co/cce-browser.git` via gitsite — the local repo is the source of
 truth, there is no push remote). Read the workspace-level
@@ -22,26 +23,34 @@ Five files, ~2.6k lines:
 | `src/downloads.rs` | the chrome-side download pipeline (Servo has none) |
 | `src/settings.rs` | the per-app KDL config |
 
-## Build: this crate is the expensive one
+## Build
 
-**Building this crate builds Servo.** That costs more than the entire rest of the
-workspace combined, and the linked binary is ~175 MB. WORKSPACE.md singles this crate
-out for that reason: leave it out of `cce-ui` sweeps unless someone has decided the
-rebuild is worth it. Before starting, check whether Servo artifacts are even present
-(`ls ../target/release/deps | grep -c servo`) — if `target/` has been pruned, the next
-build is from scratch, so kick it off early and in the background.
+The **default build is the WPE WebKit browser**: seconds to compile, ~14 MB linked
+against the system `libWPEWebKit` (`pacman -S wpewebkit` is the one prerequisite).
+That default is deliberate and load-bearing — while WPE was opt-in, a routine
+featureless rebuild by another session silently reverted the installed browser to
+Servo within two days. WORKSPACE.md's old "leave cce-browser out of `cce-ui` sweeps"
+rule was about Servo's build cost and no longer applies to the default build.
 
 ```sh
-cargo build --release -p cce-browser     # scope to this crate (shared ../target/)
-cargo test -p cce-browser                # the argv-parsing tests in main.rs
+cargo build --release -p cce-browser     # WPE WebKit (default); shared ../target/
+cargo test --release -p cce-browser      # release, or it builds Servo-debug from scratch
 ccebuild install --no-build cce-browser  # install binary + desktop entry
 ```
 
+**`--no-default-features --features servo` builds the retired Servo backend**, and
+*that* is the expensive one: it compiles Servo (more than the rest of the workspace
+combined, ~175 MB binary). Only pay for it deliberately. A last-known-good Servo
+binary sits at `~/.local/state/cce/browser/cce-browser-servo-fallback`.
+
 There is **no `Makefile`** here (most siblings have one) — install goes
 through `ccebuild` directly. `Cargo.lock` is gitignored in this crate. Running needs a
 live Wayland session; it will not run headless.
 
-`servo = "0.4"` comes from crates.io, not a git pin. Servo's embedding API churns
+The engine-specific sections below (frame pipeline, tabs, key routing) describe the
+**Servo backend** (`src/webview.rs`, feature `servo`); the WPE equivalents live in
+`src/wpe/` and are documented in WPE-PORT.md. `servo = "0.4"` comes from crates.io,
+not a git pin. Servo's embedding API churns
 hard between releases, so when a version bump breaks the build, expect the delegate
 trait, the input-event constructors, and `Preferences`/`Opts` to be where it broke.
 
diff --git a/Cargo.toml b/Cargo.toml
index bf24a33..9be486e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,21 +7,28 @@ edition = "2021"
 build = "build.rs"
 
 [features]
-# The in-progress WPE WebKit port (see WPE-PORT.md). Off by default: the
-# shipping browser is still Servo. Needs `pacman -S wpewebkit`.
+# WPE WebKit is the engine as of 2026-08-30 (see WPE-PORT.md). Default, so a
+# plain `cargo build` — including another session's sweep or reinstall —
+# ships the right engine instead of silently reverting to Servo, which is
+# exactly what happened two days after the port landed. Needs
+# `pacman -S wpewebkit`.
+default = ["wpe"]
 wpe = ["dep:rustix"]
+# The retired Servo backend, kept buildable for comparison:
+#   cargo build --release -p cce-browser --no-default-features --features servo
+servo = ["dep:servo", "dep:dpi", "dep:euclid", "dep:rustls", "dep:reqwest", "dep:http"]
 
 [dependencies]
 cce-ui = { path = "../cce-ui" }
 calloop = "0.13.0"
 wayland-client = { version = "0.31", features = ["system"] }
-servo = "0.4"
+servo = { version = "0.4", optional = true }
 url = "2"
-http = "1"
-reqwest = { version = "0.12", features = ["blocking"] }
-dpi = "0.1"
-euclid = "0.22"
-rustls = { version = "0.23", features = ["aws-lc-rs"] }
+http = { version = "1", optional = true }
+reqwest = { version = "0.12", features = ["blocking"], optional = true }
+dpi = { version = "0.1", optional = true }
+euclid = { version = "0.22", optional = true }
+rustls = { version = "0.23", features = ["aws-lc-rs"], optional = true }
 log = "0.4"
 env_logger = "0.11"
 # Only used by the `wpe` backend, to hold GLib's changing pollfd set in one
diff --git a/WPE-PORT.md b/WPE-PORT.md
index 53485b9..1f606e4 100644
--- a/WPE-PORT.md
+++ b/WPE-PORT.md
@@ -1,13 +1,17 @@
 # Porting cce-browser from Servo to WPE WebKit
 
-Status as of 2026-08-28: **the port runs.** `cargo build --release -p cce-browser
---features wpe` produces a working WebKit browser — chrome, rendering, input,
-navigation, tabs, `cce:` pages, persistent cookies, downloads. It is **not the
-default**: the shipping browser is still Servo, and every step was verified to leave
-that build and its tests untouched.
+Status as of 2026-08-30: **WPE is the default engine.** A plain
+`cargo build --release -p cce-browser` produces the WebKit browser; the Servo backend
+survives behind `--no-default-features --features servo`.
+
+The flip was forced by an incident, not a ceremony: while WPE was opt-in, a routine
+featureless rebuild by another session silently reverted the installed browser to
+Servo — two days after the port landed, with the user's WebKit-stored logins invisible
+and the interstitial memory leak live again. An opt-in engine cannot survive a
+multi-session workspace; defaults are what other sessions build.
 
 ```sh
-cargo build --release -p cce-browser --features wpe
+cargo build --release -p cce-browser
 ```
 
 Written 2026-08-27 as a scoping document; kept as the record of what the port
diff --git a/examples/wpe_dark.rs b/examples/wpe_dark.rs
index 1bb7073..f5eeb86 100644
--- a/examples/wpe_dark.rs
+++ b/examples/wpe_dark.rs
@@ -4,6 +4,10 @@
 #[cfg(not(feature = "wpe"))]
 fn main() { eprintln!("build with --features wpe"); }
 
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
 #[cfg(feature = "wpe")]
 #[path = "../src/pages.rs"]
 mod pages;
diff --git a/examples/wpe_host.rs b/examples/wpe_host.rs
index c757c4c..7f7b980 100644
--- a/examples/wpe_host.rs
+++ b/examples/wpe_host.rs
@@ -8,6 +8,10 @@ fn main() {
     eprintln!("build with --features wpe");
 }
 
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
 #[cfg(feature = "wpe")]
 #[path = "../src/pages.rs"]
 mod pages;
diff --git a/examples/wpe_input.rs b/examples/wpe_input.rs
index d1adec4..84f1ecf 100644
--- a/examples/wpe_input.rs
+++ b/examples/wpe_input.rs
@@ -11,6 +11,10 @@ fn main() {
     eprintln!("build with --features wpe");
 }
 
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
 #[cfg(feature = "wpe")]
 #[path = "../src/pages.rs"]
 mod pages;
diff --git a/examples/wpe_loop.rs b/examples/wpe_loop.rs
index f0fcf40..6b01171 100644
--- a/examples/wpe_loop.rs
+++ b/examples/wpe_loop.rs
@@ -15,6 +15,10 @@ fn main() {
     eprintln!("build with --features wpe");
 }
 
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
 #[cfg(feature = "wpe")]
 #[path = "../src/pages.rs"]
 mod pages;
diff --git a/examples/wpe_tabs.rs b/examples/wpe_tabs.rs
index cf99535..5ec72d5 100644
--- a/examples/wpe_tabs.rs
+++ b/examples/wpe_tabs.rs
@@ -12,6 +12,10 @@ fn main() {
     eprintln!("build with --features wpe");
 }
 
+#[cfg(feature = "wpe")]
+#[derive(Debug, Clone, Copy)]
+pub enum EditingCommand { Copy, Cut, Paste }
+
 #[cfg(feature = "wpe")]
 #[path = "../src/pages.rs"]
 mod pages;
diff --git a/src/downloads.rs b/src/downloads.rs
index 7d406a9..b404f29 100644
--- a/src/downloads.rs
+++ b/src/downloads.rs
@@ -147,7 +147,9 @@ fn human_size(bytes: u64) -> String {
 }
 
 impl Downloads {
-    /// Start fetching `url` on a worker thread.
+    /// Start fetching `url` on a worker thread. Servo path only: WebKit does
+    /// its own fetching and enters the store through [`Downloads::adopt`].
+    #[cfg(feature = "servo")]
     pub fn start(self: &Arc<Self>, url: Url) {
         let dir = download_dir();
         let _ = std::fs::create_dir_all(&dir);
@@ -250,6 +252,7 @@ impl Downloads {
         }
     }
 
+    #[cfg(feature = "servo")]
     fn fetch(&self, id: u64, url: Url, path: PathBuf) -> Result<(), String> {
         let client = reqwest::blocking::Client::builder()
             .user_agent(concat!("cce-browser/", env!("CARGO_PKG_VERSION")))
diff --git a/src/main.rs b/src/main.rs
index 96210a8..e74f68b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,8 @@ mod downloads;
 mod lineedit;
 mod pages;
 mod settings;
+/// The retired Servo backend; compiled only under `--features servo`.
+#[cfg(feature = "servo")]
 mod webview;
 /// The in-progress WPE WebKit backend (see WPE-PORT.md). Compiled only under
 /// `--features wpe`; the shipping browser is still Servo.
@@ -25,10 +27,15 @@ use cce_ui::scene::paint::{DisplayList, PaintCtx};
 use cce_ui::widget::display::measure_text_width;
 use cce_ui::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey};
 
-#[cfg(not(feature = "wpe"))]
+#[cfg(all(not(feature = "wpe"), feature = "servo"))]
 use webview::ServoHost as Host;
 #[cfg(feature = "wpe")]
 use wpe::WebKitHost as Host;
+#[cfg(not(any(feature = "wpe", feature = "servo")))]
+compile_error!(
+    "cce-browser needs an engine: build with the default `wpe` feature \
+     (pacman -S wpewebkit), or --no-default-features --features servo"
+);
 
 /// Clipboard action, named by neither engine. Each backend maps it to its
 /// own vocabulary — Servo needs an `EditingActionEvent`, WebKit a named
@@ -347,42 +354,7 @@ fn parse_startup_arg(arg: &str, search_prefix: &str) -> Option<Url> {
     parse_url_input(arg, search_prefix)
 }
 
-fn dom_button(button: MouseButton) -> Option<servo::MouseButton> {
-    match button {
-        MouseButton::Left => Some(servo::MouseButton::Left),
-        MouseButton::Right => Some(servo::MouseButton::Right),
-        MouseButton::Middle => Some(servo::MouseButton::Middle),
-        _ => None,
-    }
-}
 
-fn dom_key(key: &Key) -> Option<servo::Key> {
-    Some(match key {
-        Key::Character(s) => servo::Key::Character(s.clone()),
-        Key::Named(NamedKey::Space) => servo::Key::Character(" ".into()),
-        Key::Named(n) => servo::Key::Named(match n {
-            NamedKey::Backspace => servo::NamedKey::Backspace,
-            NamedKey::Tab => servo::NamedKey::Tab,
-            NamedKey::Enter => servo::NamedKey::Enter,
-            NamedKey::Escape => servo::NamedKey::Escape,
-            NamedKey::ArrowDown => servo::NamedKey::ArrowDown,
-            NamedKey::ArrowLeft => servo::NamedKey::ArrowLeft,
-            NamedKey::ArrowRight => servo::NamedKey::ArrowRight,
-            NamedKey::ArrowUp => servo::NamedKey::ArrowUp,
-            NamedKey::End => servo::NamedKey::End,
-            NamedKey::Home => servo::NamedKey::Home,
-            NamedKey::PageDown => servo::NamedKey::PageDown,
-            NamedKey::PageUp => servo::NamedKey::PageUp,
-            NamedKey::Delete => servo::NamedKey::Delete,
-            NamedKey::Control => servo::NamedKey::Control,
-            NamedKey::Shift => servo::NamedKey::Shift,
-            NamedKey::Alt => servo::NamedKey::Alt,
-            NamedKey::Super => servo::NamedKey::Meta,
-            NamedKey::F5 => servo::NamedKey::F5,
-            NamedKey::Space => unreachable!(),
-        }),
-    })
-}
 
 
 
@@ -764,7 +736,7 @@ impl Application for BrowserApp {
         // Taken before `url` moves into the host.
         let url_text = url.to_string();
 
-        #[cfg(not(feature = "wpe"))]
+        #[cfg(all(not(feature = "wpe"), feature = "servo"))]
         let mut host = Host::new(sender, url, (1200, 800), settings.color_scheme.forces_dark());
         #[cfg(feature = "wpe")]
         let mut host = {
diff --git a/src/pages.rs b/src/pages.rs
index e560dbb..abdd286 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -11,13 +11,16 @@
 //! stores shared with the main thread.
 
 use std::fs::{self, OpenOptions};
+#[cfg(feature = "servo")]
 use std::future::Future;
 use std::io::Write;
 use std::path::PathBuf;
+#[cfg(feature = "servo")]
 use std::pin::Pin;
 use std::sync::{Arc, Mutex};
 use std::time::{SystemTime, UNIX_EPOCH};
 
+#[cfg(feature = "servo")]
 use servo::protocol_handler::{
     DoneChannel, FetchContext, HttpStatus, NetworkError, ProtocolHandler, Request, Response,
     ResponseBody, ResourceFetchTiming,
@@ -324,6 +327,7 @@ impl CceProtocol {
     }
 }
 
+#[cfg(feature = "servo")]
 impl ProtocolHandler for CceProtocol {
     fn load(
         &self,
diff --git a/src/settings.rs b/src/settings.rs
index c212590..3ea64ea 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -65,6 +65,7 @@ impl ColorScheme {
     }
 }
 
+#[cfg(feature = "servo")]
 impl From<ColorScheme> for servo::Theme {
     fn from(scheme: ColorScheme) -> Self {
         match scheme {