web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
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 {