use ely_domain::{ BrowserTab, DomainError, PluginId, ReadingProgressPercent, SearchEngine, SiteOrigin, UrlText, }; use url::Url; use crate::CoreError; pub(crate) fn tab_title(url: &UrlText) -> String { if let Some(title) = internal_page_title(url.as_str()) { return title.to_string(); } url.display_host() } fn internal_page_title(url: &str) -> Option<&'static str> { match url { "ely://new-tab" => Some("New Tab"), "ely://bookmarks" => Some("Bookmarks"), "ely://notes" => Some("Notes"), "ely://reading-list" => Some("Reading List"), "ely://downloads" => Some("Downloads"), "ely://history" => Some("History"), "ely://archive" => Some("Archived Tabs"), "ely://task-manager" => Some("Task Manager"), "ely://plugins" => Some("Plugin Marketplace"), url if crash_route_tab_id(url).is_some() => Some("Tab Recovery"), url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"), url if SiteOrigin::from_site_route(url).ok().flatten().is_some() => Some("Site Settings"), "ely://about" => Some("About ELY Browser"), "ely://settings" => Some("Settings"), "ely://settings/advanced" => Some("Advanced Settings"), "ely://settings/general" => Some("General Settings"), "ely://settings/appearance" => Some("Appearance Settings"), "ely://settings/sidebar-tabs" => Some("Sidebar & Tabs Settings"), "ely://settings/search" => Some("Search Settings"), "ely://settings/privacy-security" => Some("Privacy & Security Settings"), "ely://settings/downloads" => Some("Downloads Settings"), "ely://settings/spaces" => Some("Space Settings"), "ely://settings/site-permissions" => Some("Site Permissions Settings"), "ely://settings/shortcuts" => Some("Shortcut Settings"), "ely://settings/plugins" => Some("Plugin Settings"), "ely://settings/profiles" => Some("Profile Settings"), "ely://settings/sync" => Some("Sync Settings"), "ely://settings/updates" => Some("Update Settings"), "ely://sync/status" => Some("Sync Status"), _ => None, } } pub(crate) fn tab_matches_query(tab: &BrowserTab, normalized_query: &str) -> bool { tab.title().to_lowercase().contains(normalized_query) || tab.url().as_str().to_lowercase().contains(normalized_query) || tab.display_url().to_lowercase().contains(normalized_query) } pub(crate) fn records_history(url: &UrlText) -> bool { Url::parse(url.as_str()).map(|parsed_url| parsed_url.scheme() != "ely").unwrap_or(false) } pub(crate) fn new_space_name(command: &str) -> Option<&str> { command_argument(command, &["new-space ", "new space "]) } pub(crate) fn move_tab_space_name(command: &str) -> Option<&str> { command_argument( command, &["move-tab ", "move tab ", "move-tab-to-space ", "move tab to space "], ) } pub(crate) fn archive_idle_days(command: &str) -> Option { command_argument(command, &["archive-idle-tabs ", "archive idle tabs "]) .and_then(|value| value.parse().ok()) } pub(crate) fn reading_progress_percent( command: &str, ) -> Result, CoreError> { let Some(value) = command_argument( command, &[ "reading-progress ", "reading progress ", "set-reading-progress ", "set reading progress ", ], ) else { return Ok(None); }; let percent_text = value.trim().strip_suffix('%').unwrap_or(value.trim()).trim(); let percent = percent_text.parse::().map_err(|_| { DomainError::InvalidReadingProgressPercent { value: value.trim().to_string() } })?; ReadingProgressPercent::new(percent).map(Some).map_err(CoreError::from) } pub(crate) fn note_body(command: &str) -> Option<&str> { command_argument(command, &["note ", "add-note ", "add note "]) } pub(crate) fn tab_note_body(command: &str) -> Option<&str> { command_argument(command, &["tab-note ", "tab note ", "note-tab ", "note tab "]) } pub(crate) fn tab_group_name(command: &str) -> Option<&str> { command_argument(command, &["group-tab ", "group tab ", "tab-group ", "tab group "]) } pub(crate) fn rename_tab_group_name(command: &str) -> Option<&str> { command_argument( command, &["rename-tab-group ", "rename tab group ", "rename-group ", "rename group "], ) } pub(crate) fn tab_group_color_hex(command: &str) -> Option { let value = command_argument( command, &["set-tab-group-color ", "set tab group color ", "tab-group-color ", "tab group color "], )?; parse_color_hex(value) } pub(crate) fn split_group_name(command: &str) -> Option<&str> { command_argument( command, &["group-split-view ", "group split view ", "split-view-to-group ", "split view to group "], ) } pub(crate) fn new_profile_name(command: &str) -> Option<&str> { command_argument(command, &["new-profile ", "new profile "]) } pub(crate) fn new_private_profile_name(command: &str) -> Option<&str> { command_argument(command, &["new-private-profile ", "new private profile "]) } pub(crate) fn switch_profile_name(command: &str) -> Option<&str> { command_argument(command, &["switch-profile ", "switch profile "]) } fn command_argument<'a>(command: &'a str, prefixes: &[&str]) -> Option<&'a str> { let normalized_command = command.to_ascii_lowercase(); for prefix in prefixes { if normalized_command.starts_with(prefix) { let name = command[prefix.len()..].trim(); return (!name.is_empty()).then_some(name); } } None } fn parse_color_hex(value: &str) -> Option { let value = value.trim().strip_prefix('#').unwrap_or(value.trim()); if value.len() != 6 || !value.as_bytes().iter().all(u8::is_ascii_hexdigit) { return None; } u32::from_str_radix(value, 16).ok() } pub(crate) fn space_icon(name: &str) -> String { name.chars().next().map_or_else(String::new, |value| value.to_string()) } pub(crate) fn search_url(query: &str, search_engine: SearchEngine) -> Result { search_engine.search_url(query).map_err(CoreError::from) } pub(crate) fn downloads_url() -> Result { internal_page_url("ely://downloads") } pub(crate) fn bookmarks_url() -> Result { internal_page_url("ely://bookmarks") } pub(crate) fn reading_list_url() -> Result { internal_page_url("ely://reading-list") } pub(crate) fn notes_url() -> Result { internal_page_url("ely://notes") } pub(crate) fn history_url() -> Result { internal_page_url("ely://history") } pub(crate) fn archive_url() -> Result { internal_page_url("ely://archive") } pub(crate) fn task_manager_url() -> Result { internal_page_url("ely://task-manager") } pub(crate) fn plugins_url() -> Result { internal_page_url("ely://plugins") } pub(crate) fn plugin_settings_url() -> Result { internal_page_url("ely://settings/plugins") } pub(crate) fn plugin_detail_url(plugin_id: &PluginId) -> Result { let route = format!("ely://plugin/{}", plugin_id.as_str()); internal_page_url(&route) } pub(crate) fn about_url() -> Result { internal_page_url("ely://about") } pub(crate) fn settings_url() -> Result { internal_page_url("ely://settings") } pub(crate) fn shortcut_settings_url() -> Result { internal_page_url("ely://settings/shortcuts") } pub(crate) fn sync_status_url() -> Result { internal_page_url("ely://sync/status") } pub(crate) fn settings_page_url(query: &str) -> Result, CoreError> { let Some(url) = settings_page_route(query) else { return Ok(None); }; internal_page_url(url).map(Some) } struct SettingsRouteMatch { route: &'static str, exact_terms: &'static [&'static str], search_terms: &'static [&'static str], } impl SettingsRouteMatch { fn exact_match(&self, query: &str) -> bool { self.exact_terms.iter().any(|term| normalize_settings_query(term) == query) } fn search_match(&self, query: &str) -> bool { self.search_terms.iter().any(|term| settings_term_matches(query, term)) } } const SETTINGS_ROUTE_MATCHES: &[SettingsRouteMatch] = &[ SettingsRouteMatch { route: "ely://settings", exact_terms: &["settings"], search_terms: &["Settings center", "all browser settings"], }, SettingsRouteMatch { route: "ely://settings/advanced", exact_terms: &["advanced", "advanced settings", "runtime", "diagnostics", "diagnostic"], search_terms: &[ "Advanced", "Local runtime policies and audit counters.", "runtime policy", "audit counters", "diagnostics", ], }, SettingsRouteMatch { route: "ely://settings/general", exact_terms: &["general", "browser", "new tab", "new-tab", "startup"], search_terms: &[ "General", "New Tab destination and browser startup defaults.", "default new tab", "startup defaults", ], }, SettingsRouteMatch { route: "ely://settings/appearance", exact_terms: &["appearance", "theme", "visual", "design", "colors"], search_terms: &[ "Appearance", "Theme tokens, Space accent, and Profile color.", "theme tokens", "space accent", "profile color", ], }, SettingsRouteMatch { route: "ely://about", exact_terms: &["about", "about ely browser"], search_terms: &["About", "Build, runtime, license, and product information."], }, SettingsRouteMatch { route: "ely://settings/sidebar-tabs", exact_terms: &["sidebar", "tabs", "sidebar tabs", "sidebar & tabs"], search_terms: &[ "Sidebar & Tabs", "Vertical tabs, pinned area, and auto archive policy.", "vertical tabs", "pinned area", "auto archive", ], }, SettingsRouteMatch { route: "ely://settings/search", exact_terms: &["search", "search engine", "default search", "default search engine"], search_terms: &[ "Search", "Default search engine for Command Bar queries.", "command bar search", "query provider", ], }, SettingsRouteMatch { route: "ely://settings/privacy-security", exact_terms: &[ "privacy", "security", "privacy security", "privacy & security", "history", "history recording", ], search_terms: &[ "Privacy & Security", "History recording and profile-scoped privacy controls.", "profile privacy", "recording policy", ], }, SettingsRouteMatch { route: "ely://settings/downloads", exact_terms: &["download", "downloads", "download settings", "downloads settings"], search_terms: &[ "Downloads", "Profile download location and save behavior.", "download location", "save behavior", ], }, SettingsRouteMatch { route: "ely://settings/spaces", exact_terms: &["space", "spaces", "space settings", "spaces settings"], search_terms: &[ "Spaces", "Space identity, accent color, and active context.", "space identity", "active context", ], }, SettingsRouteMatch { route: "ely://settings/site-permissions", exact_terms: &[ "site permission", "site permissions", "site permissions settings", "permissions", "permission settings", ], search_terms: &[ "Site Permissions", "Profile-scoped site permissions and local audit state.", "profile scoped permissions", "local audit state", "storage persistence", ], }, SettingsRouteMatch { route: "ely://settings/shortcuts", exact_terms: &["shortcut", "shortcuts", "keyboard", "keyboard shortcuts"], search_terms: &[ "Shortcuts", "Registered key bindings and conflict state.", "key bindings", "keyboard conflict", "cmd comma", "ctrl comma", "cmd shift p", "ctrl shift p", ], }, SettingsRouteMatch { route: "ely://settings/sync", exact_terms: &["sync", "sync settings"], search_terms: &["Sync", "Local sync state and object scope.", "sync object scope"], }, SettingsRouteMatch { route: "ely://settings/updates", exact_terms: &["update", "updates", "auto update", "auto updates", "release", "releases"], search_terms: &[ "Updates", "Build identity and Elydora release manifest contract.", "build identity", "release manifest", "artifact integrity", ], }, SettingsRouteMatch { route: "ely://settings/profiles", exact_terms: &["profile", "profiles", "profile settings", "profiles settings"], search_terms: &[ "Profiles", "Profile identity, color, and download policy.", "profile identity", "profile download policy", ], }, SettingsRouteMatch { route: "ely://settings/plugins", exact_terms: &["plugin", "plugins", "plugin settings", "plugins settings"], search_terms: &[ "Plugins", "Installed plugin control and audit trail.", "installed plugin control", "plugin audit trail", ], }, ]; fn settings_page_route(query: &str) -> Option<&'static str> { let normalized_query = normalize_settings_query(query); if normalized_query.is_empty() { return None; } SETTINGS_ROUTE_MATCHES .iter() .find(|route| route.exact_match(&normalized_query)) .or_else(|| { SETTINGS_ROUTE_MATCHES.iter().find(|route| route.search_match(&normalized_query)) }) .map(|route| route.route) } fn settings_term_matches(query: &str, term: &str) -> bool { let normalized_term = normalize_settings_query(term); query == normalized_term || query.split_whitespace().all(|part| normalized_term.contains(part)) } fn normalize_settings_query(value: &str) -> String { value .chars() .map( |character| { if character.is_ascii_alphanumeric() { character.to_ascii_lowercase() } else { ' ' } }, ) .collect::() .split_whitespace() .collect::>() .join(" ") } fn internal_page_url(value: &str) -> Result { UrlText::parse(value).map_err(CoreError::from) } fn plugin_detail_route_id(url: &str) -> Option<&str> { let plugin_id = url.strip_prefix("ely://plugin/")?; (!plugin_id.is_empty() && !plugin_id.contains('/')).then_some(plugin_id) } pub(crate) fn crash_route_tab_id(url: &str) -> Option<&str> { let tab_id = url.strip_prefix("ely://crash/")?; (!tab_id.is_empty() && !tab_id.contains('/')).then_some(tab_id) }