Skip to main content

lingxia_browser_shell/
lib.rs

1//! Shell product module and host registrations for LingXia.
2//!
3//! This crate owns product-level shell behavior on top of the generic
4//! runtime crates: address-bar resolution, downloads, settings, panels,
5//! and bundled browser assets. Platform SDK crates own their native shell UI.
6
7extern crate self as lingxia;
8
9mod address_bar;
10mod downloads;
11mod facade;
12mod panel;
13mod platform_error;
14#[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
15mod proxy;
16#[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
17mod proxy_settings;
18mod settings;
19
20pub use address_bar::{resolve_input, resolve_input_json};
21pub use facade::{
22    APP_ID, classify_navigation, classify_navigation_json, close, download, open, open_for_app,
23    should_hide_url, tab_path, update_tab,
24};
25use lingxia_browser::LxAppError;
26pub use lingxia_browser::{
27    BrowserAddressAction, BrowserAddressInputContext, BrowserAddressInputRequest,
28    BrowserAddressInputResponse, BrowserAddressInputTrigger, BrowserAddressValueKind,
29    BrowserNavigationPolicyDecision, BrowserNavigationPolicyRequest,
30    BrowserNavigationPolicyResponse, BrowserTabInfo,
31};
32#[doc(hidden)]
33pub use lingxia_native_macros::native;
34use lingxia_platform::traits::app_runtime::AppRuntime;
35#[doc(hidden)]
36pub use lxapp::LxApp;
37#[doc(hidden)]
38pub use lxapp::host;
39pub use panel::{open_panel_lxapp, panel_item_for_id, panels_config_json};
40use serde::Deserialize;
41use std::collections::BTreeMap;
42use std::io::Read;
43
44const BROWSER_WEBUI_MANIFEST_ASSET_PATH: &str = "app.lingxia.browser/lxapp.json";
45const BROWSER_CONTEXT_MENU_ASSET_PATH: &str = "app.lingxia.browser/public/browser-context-menu.js";
46
47#[derive(Debug, Deserialize)]
48struct BrowserWebUiManifest {
49    #[serde(default)]
50    pages: Vec<BrowserWebUiPage>,
51}
52
53#[derive(Debug, Deserialize)]
54struct BrowserWebUiPage {
55    name: String,
56    path: String,
57}
58
59fn parse_internal_pages(manifest_json: &str) -> Result<BTreeMap<String, String>, LxAppError> {
60    serde_json::from_str::<BrowserWebUiManifest>(manifest_json)
61        .map(|manifest| {
62            manifest
63                .pages
64                .into_iter()
65                .map(|page| (page.name, page.path))
66                .collect()
67        })
68        .map_err(|err| {
69            LxAppError::InvalidJsonFile(format!("{}: {}", BROWSER_WEBUI_MANIFEST_ASSET_PATH, err))
70        })
71}
72
73fn read_browser_asset_text(asset_path: &str) -> Result<String, LxAppError> {
74    let runtime = lxapp::get_platform().ok_or_else(|| {
75        LxAppError::Runtime(
76            "browser asset loading requires an initialized host runtime".to_string(),
77        )
78    })?;
79    let mut reader = runtime.read_asset(asset_path).map_err(|err| {
80        LxAppError::ResourceNotFound(format!("browser asset {} ({})", asset_path, err))
81    })?;
82    let mut content = String::new();
83    reader
84        .read_to_string(&mut content)
85        .map_err(|err| LxAppError::IoError(format!("failed to read {}: {}", asset_path, err)))?;
86    Ok(content)
87}
88
89fn bundled_internal_pages() -> Result<BTreeMap<String, String>, LxAppError> {
90    let manifest = read_browser_asset_text(BROWSER_WEBUI_MANIFEST_ASSET_PATH)?;
91    parse_internal_pages(&manifest)
92}
93
94fn bundled_context_menu_script() -> Result<String, LxAppError> {
95    read_browser_asset_text(BROWSER_CONTEXT_MENU_ASSET_PATH)
96}
97
98#[doc(hidden)]
99pub fn register_runtime() {
100    lingxia_browser::install_runtime();
101    downloads::register();
102    #[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
103    proxy::register();
104    settings::register();
105}
106
107#[doc(hidden)]
108pub fn register_bundled_assets() {
109    match bundled_internal_pages() {
110        Ok(internal_pages) => {
111            // Upgrade the browser host from Synthetic to a real asset bundle so the
112            // lingxia:// scheme can serve newtab/settings/downloads pages.
113            lxapp::register_builtin_asset_bundle(lingxia_browser::BUILTIN_BROWSER_APPID);
114            for (route, entry_asset) in internal_pages {
115                if let Err(err) = lingxia_browser::register_internal_page(route, entry_asset) {
116                    lxapp::warn!(
117                        "[InternalBrowser] failed to register bundled browser page: {}",
118                        err
119                    );
120                }
121            }
122        }
123        Err(err) => {
124            panic!(
125                "browser-shell requires bundled browser webui manifest at {}: {}",
126                BROWSER_WEBUI_MANIFEST_ASSET_PATH, err
127            );
128        }
129    }
130
131    match bundled_context_menu_script() {
132        Ok(script) => lingxia_browser::register_startup_page_script(script),
133        Err(err) => {
134            lxapp::info!(
135                "[InternalBrowser] bundled browser context menu unavailable; skipping startup script: {}",
136                err
137            );
138        }
139    }
140}
141
142#[doc(hidden)]
143pub fn warmup() {
144    #[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
145    proxy::warmup();
146    lingxia_browser::warmup();
147}
148
149#[cfg(test)]
150mod tests {
151    use super::parse_internal_pages;
152
153    #[test]
154    fn parses_named_internal_pages_manifest() {
155        let pages = parse_internal_pages(
156            r#"{
157                "pages": [
158                    { "name": "newtab", "path": "pages/newtab/index.html" },
159                    { "name": "downloads", "path": "pages/downloads/index.html" },
160                    { "name": "settings", "path": "pages/settings/index.html" }
161                ]
162            }"#,
163        )
164        .expect("manifest should parse");
165        assert_eq!(
166            pages.get("newtab").map(String::as_str),
167            Some("pages/newtab/index.html")
168        );
169        assert_eq!(
170            pages.get("downloads").map(String::as_str),
171            Some("pages/downloads/index.html")
172        );
173        assert_eq!(
174            pages.get("settings").map(String::as_str),
175            Some("pages/settings/index.html")
176        );
177    }
178
179    #[test]
180    fn rejects_legacy_ordered_pages_manifest() {
181        assert!(parse_internal_pages(r#"{ "pages": ["pages/newtab/index.html"] }"#).is_err());
182    }
183}