diff --git a/Cargo.lock b/Cargo.lock index fc05d080a..14609e8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,6 +885,7 @@ dependencies = [ "redoxer", "regex", "serde", + "serde_json", "strip-ansi-escapes", "termion", "toml", @@ -1018,24 +1019,47 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1458,3 +1482,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index ad32286ff..91e9cca2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,12 +42,13 @@ redox-pkg = { git = "https://gitlab.redox-os.org/redox-os/pkgutils.git", default redox_installer = { git = "https://gitlab.redox-os.org/redox-os/installer.git", default-features = false } redoxer = { git = "https://gitlab.redox-os.org/redox-os/redoxer.git", default-features = false } regex = "1.11" -serde = { version = "=1.0.197", features = ["derive"] } +serde = { version = "1", features = ["derive"] } termion = "4" toml = "0.8" walkdir = "2.3.1" ansi-to-tui = { version = "8", optional = true } strip-ansi-escapes = { version = "0.2.1", optional = true } +serde_json = "1" [dependencies.ratatui] version = "0.30" diff --git a/src/web.rs b/src/web.rs index e669bc8ad..922483e26 100644 --- a/src/web.rs +++ b/src/web.rs @@ -13,6 +13,7 @@ use crate::{ }; pub mod html; +pub mod search; #[derive(Clone)] pub struct CliWebConfig { @@ -49,6 +50,7 @@ impl CliWebConfig { } const CSS: &str = include_str!("./web/style.css"); +const FILES: &str = include_str!("./web/files.html"); pub fn generate_web(all_packages: &Vec, config: &CliWebConfig) { let repo_path = &config.out_dir.join(redoxer::target()); @@ -58,6 +60,7 @@ pub fn generate_web(all_packages: &Vec, config: &CliWebConfig) { let mut valid_packages = Vec::new(); let mut dependents_map: HashMap> = HashMap::new(); + let mut files_map: BTreeMap = BTreeMap::new(); for package_name in all_packages { let Ok(package_name) = PackageName::new(package_name) else { @@ -108,6 +111,10 @@ pub fn generate_web(all_packages: &Vec, config: &CliWebConfig) { &html_path, &config, ); + + if let Some(file_map) = stage_files { + files_map.insert(package.name.to_string(), file_map); + } } let mut grouped_packages: BTreeMap> = BTreeMap::new(); @@ -120,7 +127,18 @@ pub fn generate_web(all_packages: &Vec, config: &CliWebConfig) { let index_path = repo_path.join("index.html"); let style_path = repo_path.join("style.css"); generate_html_index(grouped_packages, &index_path, &config); - fs::write(style_path, CSS).expect("Failed to write CSS file"); + fs::write(style_path, CSS).expect("Failed to write style.css"); + + let mut index_files = search::FileIndexBuilder::new(); + for (package, files) in &files_map { + index_files.parse(package, files); + } + let files_json = repo_path.join("files.json"); + index_files + .write(&files_json) + .expect("Failed to write files.json"); + let files_path = repo_path.join("files.html"); + fs::write(files_path, FILES).expect("Failed to write files.html"); } pub(crate) fn get_category(dir: &Path) -> String { diff --git a/src/web/files.html b/src/web/files.html new file mode 100644 index 000000000..f4dccde1b --- /dev/null +++ b/src/web/files.html @@ -0,0 +1,401 @@ + + + + + + + Package File Browser + + + + +
+

Package File Browser

+ +
+ + +
+
+
/
+
+
Loading files...
+
+
+
+
Files
+
+
+
+
File Content
+
+
+ Select a file to reveal details. +
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/web/search.rs b/src/web/search.rs new file mode 100644 index 000000000..85653709b --- /dev/null +++ b/src/web/search.rs @@ -0,0 +1,113 @@ +use serde::Serialize; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use crate::{Result, wrap_io_err}; + +#[derive(Serialize, Debug, Clone)] +#[serde(untagged)] +enum FsNode { + Dir(BTreeMap), + File(String), +} + +pub struct FileIndexBuilder { + inner: BTreeMap, +} +impl FileIndexBuilder { + pub const fn new() -> Self { + Self { + inner: BTreeMap::new(), + } + } + fn insert_node(&mut self, path_stack: &[String], name: &str, pkg_name: Option<&str>) { + let mut current = &mut self.inner; + for dir in path_stack { + let node = current + .entry(dir.clone()) + .or_insert_with(|| FsNode::Dir(BTreeMap::new())); + + if let FsNode::File(_) = node { + // previously a file, silently replace to dir + *node = FsNode::Dir(BTreeMap::new()); + } + + match node { + FsNode::Dir(map) => current = map, + _ => unreachable!(), + } + } + + if let Some(pkg) = pkg_name { + current + .entry(name.to_string()) + .and_modify(|node| { + if let FsNode::File(existing_pkgs) = node { + existing_pkgs.push(','); + existing_pkgs.push_str(pkg); + } else { + *node = FsNode::File(pkg.to_string()); + } + }) + .or_insert_with(|| FsNode::File(pkg.to_string())); + } else { + current + .entry(name.to_string()) + .or_insert_with(|| FsNode::Dir(BTreeMap::new())); + } + } + pub fn parse(&mut self, pkg: &str, content: &str) { + let mut path_stack: Vec = Vec::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + + let mut prefix_chars: usize = 0; + let mut name_start_byte = 0; + + for (idx, c) in line.char_indices() { + if "│├└─ ".contains(c) { + prefix_chars += 1; + } else { + name_start_byte = idx; + break; + } + } + + if name_start_byte == 0 && prefix_chars == 0 { + continue; + } + + let depth = (prefix_chars / 4).saturating_sub(1); + let remainder = line[name_start_byte..].trim(); + + path_stack.truncate(depth); + + if remainder.ends_with('/') { + let name = remainder.trim_end_matches('/'); + self.insert_node(&path_stack, name, None); + path_stack.push(name.to_string()); + } else { + let name = if let Some(paren_idx) = remainder.rfind('(') { + // Strip size + remainder[..paren_idx].trim() + } else { + remainder + }; + self.insert_node(&path_stack, name, Some(pkg)); + } + } + } + pub fn write(&self, path: &Path) -> Result<()> { + // JSON: because javascript have them natively + let json_output = serde_json::to_string(&self.inner).unwrap(); + let mut file = File::create(path).map_err(wrap_io_err!(path, "Opening file"))?; + file.write_all(json_output.as_bytes()) + .map_err(wrap_io_err!(path, "Writing file"))?; + Ok(()) + } +} diff --git a/src/web/style.css b/src/web/style.css index 560735ea6..87773f551 100644 --- a/src/web/style.css +++ b/src/web/style.css @@ -43,11 +43,9 @@ body { border-radius: 6px; padding: 15px; margin: 10px; - display: inline-block; width: 30%; vertical-align: top; - display: flex; flex: 0 1 280px; flex-direction: column; @@ -162,7 +160,7 @@ code { } .install-action { - display: inline-block; + display: inline-block; background-color: #fff; border: 1px solid #ddd; border-radius: 6px; @@ -191,7 +189,7 @@ code { } .pkg-main, .pkg-meta { - width: 100%; + width: 100%; } @media (min-width: 768px) { @@ -245,13 +243,90 @@ th { width: 50%; } +.search-box { + width: 100%; + padding: 12px 15px; + font-size: 1.1rem; + border: 1px solid #ddd; + border-radius: 6px; + margin-bottom: 20px; + box-sizing: border-box; + font-family: inherit; +} + +.browser-panels { + display: flex; + gap: 20px; + height: 80vh; + min-height: 400px; +} + +.panel { + flex: 1; + border: 1px solid #eee; + border-radius: 6px; + display: flex; + flex-direction: column; + background-color: #fff; + overflow: hidden; +} + +.panel-header { + font-weight: 600; + padding: 10px 15px; + border-bottom: 1px solid #eee; + background-color: #f9f9f9; + font-family: ui-monospace, SFMono-Regular, monospace; + font-size: 0.9rem; +} + +.panel-body { + flex: 1; + overflow-y: auto; + padding: 10px 0; +} + +.list-item { + padding: 8px 15px; + cursor: pointer; + display: flex; + align-items: center; + user-select: none; +} + +.list-item:hover { + background-color: #f1f8ff; +} + +.list-item.selected { + background-color: #0366d6; + color: #fff; +} + +.list-item.selected .pkg-badge { + color: #fff; + background-color: rgba(255, 255, 255, 0.2); +} + +.pkg-badge { + font-size: 0.75rem; + color: #6a737d; + background-color: #eee; + padding: 2px 6px; + border-radius: 12px; +} +.up-icon::before { + content: "↵ "; + margin-right: 0.2em; +} + @media (prefers-color-scheme: dark) { body { background-color: #000; color: #ccc; } - .package-card, .card, .pkg-header, .index-header { + .package-card, .card, .panel, .pkg-header, .index-header { background-color: #111; border-color: #333; } @@ -289,4 +364,30 @@ th { th, td, .pkg-deps li, .pkg-dependents li { border-bottom-color: #333; } -} + + .search-box { + background-color: #000; + color: #ccc; + border-color: #333; + } + + .panel-header { + background-color: #1a1a1a; + border-bottom-color: #333; + color: #8b949e; + } + + .list-item:hover { + background-color: #222; + } + + .list-item.selected { + background-color: #1f6feb; + color: #fff; + } + + .pkg-badge { + background-color: #333; + color: #8b949e; + } +} \ No newline at end of file