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

commit6d899fdb1be46602c78e9a00e75adca004732a97
parent09551ae8ad
authorLucas Galante <[email protected]>
date2026-08-12 10:40
Read settings from the app config; reload on window focus

New settings.rs reads ~/.config/cce/cce-browser/config.kdl (browser
section, written by cce-system-interface's new Browser page): homepage
replaces the hardcoded start URL, the configured engine drives the
URL-bar search fallback, download-dir overrides the XDG default (a
global override, since downloads run on worker threads), and history
gates visit recording. Settings re-load when the window regains focus,
so edits in system-interface apply on the next switch back.

Live-verified end to end: homepage launch, Wikipedia search, history
off (no records), history re-enabled via focus reload (recorded).

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

 src/downloads.rs | 15 ++++++++++--
 src/main.rs      | 42 +++++++++++++++++++++++++++-------
 src/settings.rs  | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/webview.rs   | 11 ++++++++-
 4 files changed, 126 insertions(+), 11 deletions(-)

diff --git a/src/downloads.rs b/src/downloads.rs
index 08d336e..9bf942a 100644
--- a/src/downloads.rs
+++ b/src/downloads.rs
@@ -57,9 +57,20 @@ pub struct Downloads {
     next_id: std::sync::atomic::AtomicU64,
 }
 
-/// The user's download directory: XDG_DOWNLOAD_DIR from user-dirs.dirs
-/// when configured, else ~/Downloads.
+/// Settings override for the download directory (None = XDG default).
+/// A global because downloads run on worker threads.
+static DIR_OVERRIDE: Mutex<Option<PathBuf>> = Mutex::new(None);
+
+pub fn set_download_dir(dir: Option<PathBuf>) {
+    *DIR_OVERRIDE.lock().unwrap() = dir;
+}
+
+/// The user's download directory: the settings override when set, else
+/// XDG_DOWNLOAD_DIR from user-dirs.dirs, else ~/Downloads.
 fn download_dir() -> PathBuf {
+    if let Some(dir) = DIR_OVERRIDE.lock().unwrap().clone() {
+        return dir;
+    }
     let home = PathBuf::from(std::env::var("HOME").unwrap_or_default());
     let conf = home.join(".config/user-dirs.dirs");
     if let Ok(text) = std::fs::read_to_string(conf) {
diff --git a/src/main.rs b/src/main.rs
index 5caf6b0..a14e79f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@
 
 mod downloads;
 mod pages;
+mod settings;
 mod webview;
 
 use url::Url;
@@ -47,7 +48,6 @@ const URL_PAD_X: f32 = 9.0;
 /// Pixels per wheel notch when the DE reports discrete line deltas.
 const LINE_PX: f64 = 76.0;
 
-const HOME_URL: &str = "https://servo.org";
 
 const PAGE_BG: [f32; 4] = [0.10, 0.10, 0.11, 1.0];
 const FIELD_BG: [f32; 4] = [0.09, 0.09, 0.10, 0.40];
@@ -70,6 +70,8 @@ pub enum Message {
 
 struct BrowserApp {
     host: ServoHost,
+    /// Loaded from the app config; re-read when the window regains focus.
+    settings: settings::Settings,
     win: (f32, f32),
     scale: f64,
     pointer: (f32, f32),
@@ -173,7 +175,7 @@ fn url_rect(win_w: f32) -> Rect {
 
 /// Turn URL-bar input into something loadable: a real URL as-is, a bare
 /// host gets https://, anything else becomes a search.
-fn parse_url_input(input: &str) -> Option<Url> {
+fn parse_url_input(input: &str, search_prefix: &str) -> Option<Url> {
     let s = input.trim();
     if s.is_empty() {
         return None;
@@ -198,7 +200,7 @@ fn parse_url_input(input: &str) -> Option<Url> {
         }
     }
     let q: String = url::form_urlencoded::byte_serialize(s.as_bytes()).collect();
-    Url::parse(&format!("https://duckduckgo.com/html/?q={q}")).ok()
+    Url::parse(&format!("{search_prefix}{q}")).ok()
 }
 
 fn dom_button(button: MouseButton) -> Option<servo::MouseButton> {
@@ -283,7 +285,7 @@ impl BrowserApp {
     }
 
     fn navigate(&mut self) {
-        if let Some(url) = parse_url_input(&self.url_input) {
+        if let Some(url) = parse_url_input(&self.url_input, &self.settings.search_prefix) {
             self.host.load(url);
             self.url_focused = false;
             self.loading = true;
@@ -291,6 +293,18 @@ impl BrowserApp {
     }
 
     /// New blank tab with the URL bar focused for typing.
+    /// Pick up settings edits (system-interface, cce-data-editor) when the
+    /// window regains focus.
+    fn reload_settings(&mut self) {
+        let new = settings::load();
+        if new == self.settings {
+            return;
+        }
+        downloads::set_download_dir(new.download_dir.clone());
+        self.host.set_history_enabled(new.history);
+        self.settings = new;
+    }
+
     fn new_tab(&mut self) {
         let url = Url::parse("about:blank").expect("about:blank");
         self.host.open_tab(url);
@@ -417,16 +431,22 @@ impl Application for BrowserApp {
     type Message = Message;
 
     fn new(_qh: &QueueHandle<EngineState<Self>>, sender: calloop::channel::Sender<Self::Message>) -> Self {
-        // Optional CLI arg: the start URL (same parsing as the URL bar).
+        let settings = settings::load();
+        downloads::set_download_dir(settings.download_dir.clone());
+        // Optional CLI arg: the start URL (same parsing as the URL bar);
+        // otherwise the configured homepage.
         let url = std::env::args()
             .nth(1)
-            .and_then(|arg| parse_url_input(&arg))
-            .unwrap_or_else(|| Url::parse(HOME_URL).expect("home url"));
+            .and_then(|arg| parse_url_input(&arg, &settings.search_prefix))
+            .or_else(|| parse_url_input(&settings.homepage, &settings.search_prefix))
+            .unwrap_or_else(|| Url::parse(settings::DEFAULT_HOMEPAGE).expect("home url"));
         let url_input = url.to_string();
         let cursor = url_input.len();
-        let host = ServoHost::new(sender, url, (1200, 800));
+        let mut host = ServoHost::new(sender, url, (1200, 800));
+        host.set_history_enabled(settings.history);
         Self {
             host,
+            settings,
             win: (1200.0, 800.0),
             scale: 1.0,
             pointer: (0.0, 0.0),
@@ -469,6 +489,12 @@ impl Application for BrowserApp {
 
     fn tick(&mut self, _dt: f32, _needs_rebuild: &mut bool) {}
 
+    fn handle_focus_change(&mut self, focused: bool, _needs_rebuild: &mut bool) {
+        if focused {
+            self.reload_settings();
+        }
+    }
+
     fn handle_resize(&mut self, width: f32, height: f32, scale: f64) {
         self.win = (width, height);
         self.scale = scale;
diff --git a/src/settings.rs b/src/settings.rs
new file mode 100644
index 0000000..b788c45
--- /dev/null
+++ b/src/settings.rs
@@ -0,0 +1,69 @@
+//! Browser settings from the per-app cce config
+//! (`~/.config/cce/cce-browser/config.kdl`, section `browser`) — the file
+//! cce-system-interface's Browser page edits. Loaded at startup and
+//! re-read when the window regains focus, so settings changed in
+//! system-interface apply on the next switch back to the browser.
+
+use std::path::PathBuf;
+
+pub const DEFAULT_HOMEPAGE: &str = "https://servo.org";
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Settings {
+    pub homepage: String,
+    /// Query-URL prefix for the search fallback; escaped terms are appended.
+    pub search_prefix: String,
+    /// Override for the download directory (None = XDG default).
+    pub download_dir: Option<PathBuf>,
+    /// Record page visits to cce://history.
+    pub history: bool,
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        Self {
+            homepage: DEFAULT_HOMEPAGE.to_string(),
+            search_prefix: search_prefix("duckduckgo").to_string(),
+            download_dir: None,
+            history: true,
+        }
+    }
+}
+
+/// Engine keys as written by the system-interface Browser page.
+fn search_prefix(key: &str) -> &'static str {
+    match key {
+        "google" => "https://www.google.com/search?q=",
+        "bing" => "https://www.bing.com/search?q=",
+        "wikipedia" => "https://en.wikipedia.org/wiki/Special:Search?search=",
+        _ => "https://duckduckgo.com/html/?q=",
+    }
+}
+
+pub fn load() -> Settings {
+    let path = cce_ui::config::get_app_config_path("cce-browser");
+    let content = std::fs::read_to_string(path).unwrap_or_default();
+    let val = cce_ui::config::parse_kdl_to_json(&content);
+    let b = &val["browser"];
+
+    let homepage = match b["homepage"].as_str().map(str::trim) {
+        Some(h) if !h.is_empty() => h.to_string(),
+        _ => DEFAULT_HOMEPAGE.to_string(),
+    };
+    let download_dir = match b["download-dir"].as_str().map(str::trim) {
+        Some(d) if !d.is_empty() => {
+            let home = std::env::var("HOME").unwrap_or_default();
+            Some(PathBuf::from(match d.strip_prefix("~/") {
+                Some(rest) => format!("{home}/{rest}"),
+                None => d.to_string(),
+            }))
+        }
+        _ => None,
+    };
+    Settings {
+        homepage,
+        search_prefix: search_prefix(b["search"].as_str().unwrap_or("duckduckgo")).to_string(),
+        download_dir,
+        history: b["history"].as_bool().unwrap_or(true),
+    }
+}
diff --git a/src/webview.rs b/src/webview.rs
index 4031f99..51fe1bc 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -154,6 +154,14 @@ pub struct ServoHost {
     active: usize,
     size_px: (u32, u32),
     scale: f32,
+    /// Settings gate for cce://history recording.
+    history_enabled: bool,
+}
+
+impl ServoHost {
+    pub fn set_history_enabled(&mut self, on: bool) {
+        self.history_enabled = on;
+    }
 }
 
 impl ServoHost {
@@ -210,6 +218,7 @@ impl ServoHost {
             active: usize::MAX,
             size_px,
             scale: 1.0,
+            history_enabled: true,
         };
         host.open_tab(url);
         host
@@ -347,7 +356,7 @@ impl ServoHost {
                         let was_loading = tab.loading;
                         tab.loading = loading;
                         // Load-complete transition: log the visit.
-                        if was_loading && !loading {
+                        if was_loading && !loading && self.history_enabled {
                             if let Some(url) = &tab.url {
                                 self.history
                                     .record(url.as_str(), tab.title.as_deref().unwrap_or(""));