Add about internal page
This commit is contained in:
Generated
+1
@@ -2252,6 +2252,7 @@ dependencies = [
|
|||||||
"gpui-component-assets",
|
"gpui-component-assets",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -17,5 +17,8 @@ gpui-component-assets.workspace = true
|
|||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
toml.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
io::{self, Write},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
||||||
|
let workspace_root = workspace_root(&manifest_dir)?;
|
||||||
|
let workspace_manifest_path = workspace_root.join("Cargo.toml");
|
||||||
|
let git_head_path = workspace_root.join(".git/HEAD");
|
||||||
|
let workspace_manifest = read_workspace_manifest(&workspace_manifest_path)?;
|
||||||
|
let workspace = table(&workspace_manifest, "workspace")?;
|
||||||
|
let package = table(workspace, "package")?;
|
||||||
|
let dependencies = table(workspace, "dependencies")?;
|
||||||
|
|
||||||
|
emit_cargo_directive(format!("cargo:rerun-if-changed={}", workspace_manifest_path.display()))?;
|
||||||
|
emit_cargo_directive(format!("cargo:rerun-if-changed={}", git_head_path.display()))?;
|
||||||
|
if let Some(ref_path) = git_head_ref(&git_head_path)? {
|
||||||
|
emit_cargo_directive(format!(
|
||||||
|
"cargo:rerun-if-changed={}",
|
||||||
|
workspace_root.join(".git").join(ref_path).display()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_env("ELY_BUILD_REVISION", &git_revision(workspace_root)?)?;
|
||||||
|
emit_env("ELY_WORKSPACE_LICENSE", string_value(package, "license")?)?;
|
||||||
|
emit_env("ELY_GPUI_VERSION", dependency_version(dependencies, "gpui")?)?;
|
||||||
|
emit_env("ELY_GPUI_COMPONENT_VERSION", dependency_version(dependencies, "gpui-component")?)?;
|
||||||
|
emit_env("ELY_SERVO_VERSION", dependency_version(dependencies, "servo")?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root(manifest_dir: &Path) -> Result<&Path, Box<dyn Error>> {
|
||||||
|
let crates_dir = manifest_dir
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing crates directory"))?;
|
||||||
|
crates_dir
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing workspace root"))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_workspace_manifest(path: &Path) -> Result<toml::Table, Box<dyn Error>> {
|
||||||
|
let manifest = fs::read_to_string(path)?;
|
||||||
|
manifest.parse::<toml::Table>().map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table<'a>(value: &'a toml::Table, key: &str) -> Result<&'a toml::Table, Box<dyn Error>> {
|
||||||
|
value
|
||||||
|
.get(key)
|
||||||
|
.and_then(toml::Value::as_table)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("missing {key} table")))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_value<'a>(value: &'a toml::Table, key: &str) -> Result<&'a str, Box<dyn Error>> {
|
||||||
|
value
|
||||||
|
.get(key)
|
||||||
|
.and_then(toml::Value::as_str)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("missing {key} value")))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependency_version<'a>(
|
||||||
|
dependencies: &'a toml::Table,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<&'a str, Box<dyn Error>> {
|
||||||
|
let dependency = dependencies.get(name).ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, format!("missing {name} dependency"))
|
||||||
|
})?;
|
||||||
|
match dependency {
|
||||||
|
toml::Value::String(version) => Ok(version),
|
||||||
|
toml::Value::Table(table) => string_value(table, "version"),
|
||||||
|
_ => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("invalid {name} dependency version"),
|
||||||
|
)
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_revision(workspace_root: &Path) -> Result<String, Box<dyn Error>> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.args(["rev-parse", "--short=12", "HEAD"])
|
||||||
|
.current_dir(workspace_root)
|
||||||
|
.output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, "git revision is unavailable").into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let revision = String::from_utf8(output.stdout)?.trim().to_string();
|
||||||
|
if revision.is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "git revision is empty").into());
|
||||||
|
}
|
||||||
|
Ok(revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_head_ref(git_head_path: &Path) -> Result<Option<String>, Box<dyn Error>> {
|
||||||
|
let head = fs::read_to_string(git_head_path)?;
|
||||||
|
let Some(ref_path) = head.trim().strip_prefix("ref: ") else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(Some(ref_path.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_env(key: &str, value: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, format!("{key} is empty")).into());
|
||||||
|
}
|
||||||
|
emit_cargo_directive(format!("cargo:rustc-env={key}={value}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_cargo_directive(directive: impl AsRef<str>) -> Result<(), Box<dyn Error>> {
|
||||||
|
writeln!(io::stdout(), "{}", directive.as_ref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod about;
|
||||||
mod download_actions;
|
mod download_actions;
|
||||||
mod download_labels;
|
mod download_labels;
|
||||||
mod downloads;
|
mod downloads;
|
||||||
@@ -27,6 +28,7 @@ impl ElyShell {
|
|||||||
"ely://downloads" => self.render_downloads_page(snapshot, cx),
|
"ely://downloads" => self.render_downloads_page(snapshot, cx),
|
||||||
"ely://history" => self.render_history_page(snapshot, cx),
|
"ely://history" => self.render_history_page(snapshot, cx),
|
||||||
"ely://archive" => self.render_archive_page(snapshot, cx),
|
"ely://archive" => self.render_archive_page(snapshot, cx),
|
||||||
|
"ely://about" => self.render_about_page(snapshot),
|
||||||
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
||||||
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
|
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
|
||||||
"ely://settings/sync" => self.render_sync_page(snapshot),
|
"ely://settings/sync" => self.render_sync_page(snapshot),
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
use ely_browser_core::BrowserSnapshot;
|
||||||
|
use ely_design_system::colors;
|
||||||
|
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||||
|
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
||||||
|
|
||||||
|
use super::{ElyShell, render_canvas_surface};
|
||||||
|
|
||||||
|
const PRODUCT_NAME: &str = "ELY Browser";
|
||||||
|
const COMPANY_NAME: &str = "Elydora";
|
||||||
|
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
const BUILD_REVISION: &str = env!("ELY_BUILD_REVISION");
|
||||||
|
const WORKSPACE_LICENSE: &str = env!("ELY_WORKSPACE_LICENSE");
|
||||||
|
const GPUI_VERSION: &str = env!("ELY_GPUI_VERSION");
|
||||||
|
const GPUI_COMPONENT_VERSION: &str = env!("ELY_GPUI_COMPONENT_VERSION");
|
||||||
|
const SERVO_VERSION: &str = env!("ELY_SERVO_VERSION");
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn render_about_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
|
render_canvas_surface(
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.p_8()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_5()
|
||||||
|
.child(render_about_header())
|
||||||
|
.child(render_about_rows(snapshot)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_about_header() -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_end()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(26.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(format!("About {PRODUCT_NAME}")),
|
||||||
|
)
|
||||||
|
.child(div().text_sm().text_color(rgb(colors::MUTED)).child(COMPANY_NAME)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(IconName::Info)
|
||||||
|
.child(format!("Build {BUILD_REVISION}")),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_about_rows(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.min_h_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.child(about_row(IconName::Building2, "Product", PRODUCT_NAME, COMPANY_NAME))
|
||||||
|
.child(about_row(IconName::Info, "Version", APP_VERSION, "Cargo package version"))
|
||||||
|
.child(about_row(IconName::GitHub, "Build", BUILD_REVISION, "Git revision"))
|
||||||
|
.child(about_row(
|
||||||
|
IconName::Frame,
|
||||||
|
"GPUI",
|
||||||
|
format!("gpui {GPUI_VERSION}"),
|
||||||
|
"Native desktop renderer",
|
||||||
|
))
|
||||||
|
.child(about_row(
|
||||||
|
IconName::LayoutDashboard,
|
||||||
|
"Components",
|
||||||
|
format!("gpui-component {GPUI_COMPONENT_VERSION}"),
|
||||||
|
"UI component toolkit",
|
||||||
|
))
|
||||||
|
.child(about_row(
|
||||||
|
IconName::Globe,
|
||||||
|
"Servo",
|
||||||
|
format!("servo {SERVO_VERSION}"),
|
||||||
|
"Browser engine crate",
|
||||||
|
))
|
||||||
|
.child(about_row(
|
||||||
|
IconName::BookOpen,
|
||||||
|
"License",
|
||||||
|
WORKSPACE_LICENSE,
|
||||||
|
"Workspace package license",
|
||||||
|
))
|
||||||
|
.child(about_row(
|
||||||
|
IconName::CircleUser,
|
||||||
|
"Runtime",
|
||||||
|
&snapshot.active_profile_name,
|
||||||
|
format!("{} - {} open tabs", snapshot.active_space_name, snapshot.tabs.len()),
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_row(
|
||||||
|
icon: IconName,
|
||||||
|
label: &'static str,
|
||||||
|
value: impl Into<String>,
|
||||||
|
detail: impl Into<String>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let value = value.into();
|
||||||
|
let detail = detail.into();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.py_3()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(rgb(colors::HAIRLINE))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_3()
|
||||||
|
.child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(label),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div().text_xs().truncate().text_color(rgb(colors::MUTED)).child(detail),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w(px(300.0))
|
||||||
|
.truncate()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(value),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
|
|||||||
"ely://downloads" => Some("Downloads"),
|
"ely://downloads" => Some("Downloads"),
|
||||||
"ely://history" => Some("History"),
|
"ely://history" => Some("History"),
|
||||||
"ely://archive" => Some("Archived Tabs"),
|
"ely://archive" => Some("Archived Tabs"),
|
||||||
|
"ely://about" => Some("About ELY Browser"),
|
||||||
"ely://settings" => Some("Settings"),
|
"ely://settings" => Some("Settings"),
|
||||||
"ely://settings/plugins" => Some("Plugin Settings"),
|
"ely://settings/plugins" => Some("Plugin Settings"),
|
||||||
"ely://settings/profiles" => Some("Profile Settings"),
|
"ely://settings/profiles" => Some("Profile Settings"),
|
||||||
@@ -87,6 +88,10 @@ pub(crate) fn history_url() -> Result<UrlText, CoreError> {
|
|||||||
internal_page_url("ely://history")
|
internal_page_url("ely://history")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn about_url() -> Result<UrlText, CoreError> {
|
||||||
|
internal_page_url("ely://about")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn settings_url() -> Result<UrlText, CoreError> {
|
pub(crate) fn settings_url() -> Result<UrlText, CoreError> {
|
||||||
internal_page_url("ely://settings")
|
internal_page_url("ely://settings")
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,7 @@ pub(crate) fn settings_page_url(query: &str) -> Result<Option<UrlText>, CoreErro
|
|||||||
fn settings_page_route(query: &str) -> Option<&'static str> {
|
fn settings_page_route(query: &str) -> Option<&'static str> {
|
||||||
match query {
|
match query {
|
||||||
"settings" | "general" | "browser" => Some("ely://settings"),
|
"settings" | "general" | "browser" => Some("ely://settings"),
|
||||||
|
"about" | "about ely browser" => Some("ely://about"),
|
||||||
"sync" | "sync settings" => Some("ely://settings/sync"),
|
"sync" | "sync settings" => Some("ely://settings/sync"),
|
||||||
"profile" | "profiles" | "profile settings" | "profiles settings" => {
|
"profile" | "profiles" | "profile settings" | "profiles settings" => {
|
||||||
Some("ely://settings/profiles")
|
Some("ely://settings/profiles")
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use ely_domain::{CommandIntent, CommandScope, ProfileId, ProfileKind, SpaceId};
|
|||||||
use crate::{
|
use crate::{
|
||||||
CoreError,
|
CoreError,
|
||||||
navigation::{
|
navigation::{
|
||||||
downloads_url, history_url, move_tab_space_name, new_profile_name, new_space_name,
|
about_url, downloads_url, history_url, move_tab_space_name, new_profile_name,
|
||||||
search_url, settings_page_url, settings_url, space_icon, switch_profile_name,
|
new_space_name, search_url, settings_page_url, settings_url, space_icon,
|
||||||
sync_status_url,
|
switch_profile_name, sync_status_url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,6 +106,10 @@ impl BrowserCore {
|
|||||||
self.open_tab(history_url()?);
|
self.open_tab(history_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
"about" | "open-about" | "open about" => {
|
||||||
|
self.open_tab(about_url()?);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
"settings" | "open-settings" | "open settings" => {
|
"settings" | "open-settings" | "open settings" => {
|
||||||
self.open_tab(settings_url()?);
|
self.open_tab(settings_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@@ -81,6 +81,21 @@ fn open_history_command_opens_history_page() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_about_command_opens_about_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">about");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("about".to_string())));
|
||||||
|
assert_eq!(active_tab.title(), "About ELY Browser");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://about");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_settings_command_opens_settings_page() -> Result<(), Box<dyn Error>> {
|
fn open_settings_command_opens_settings_page() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
@@ -111,6 +126,27 @@ fn open_sync_status_command_opens_sync_status_page() -> Result<(), Box<dyn Error
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_scoped_search_opens_about_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query("@settings about");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let active_tab = core.active_tab()?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
intent,
|
||||||
|
Some(CommandIntent::ScopedSearch {
|
||||||
|
scope: CommandScope::Settings,
|
||||||
|
query: "about".to_string()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(active_tab.title(), "About ELY Browser");
|
||||||
|
assert_eq!(active_tab.url().as_str(), "ely://about");
|
||||||
|
assert_eq!(core.snapshot()?.command_query, "");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_scoped_search_opens_matching_settings_page() -> Result<(), Box<dyn Error>> {
|
fn settings_scoped_search_opens_matching_settings_page() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user