web: Implement file index

This commit is contained in:
Wildan M 2026-05-08 14:50:44 +07:00
parent 4d30dceba0
commit 771df295c5
No known key found for this signature in database
GPG Key ID: 01AC53185C679C79
4 changed files with 165 additions and 6 deletions

38
Cargo.lock generated
View File

@ -885,6 +885,7 @@ dependencies = [
"redoxer", "redoxer",
"regex", "regex",
"serde", "serde",
"serde_json",
"strip-ansi-escapes", "strip-ansi-escapes",
"termion", "termion",
"toml", "toml",
@ -1018,24 +1019,47 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "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]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.9" version = "0.6.9"
@ -1458,3 +1482,9 @@ dependencies = [
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@ -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 } 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 } redoxer = { git = "https://gitlab.redox-os.org/redox-os/redoxer.git", default-features = false }
regex = "1.11" regex = "1.11"
serde = { version = "=1.0.197", features = ["derive"] } serde = { version = "1", features = ["derive"] }
termion = "4" termion = "4"
toml = "0.8" toml = "0.8"
walkdir = "2.3.1" walkdir = "2.3.1"
ansi-to-tui = { version = "8", optional = true } ansi-to-tui = { version = "8", optional = true }
strip-ansi-escapes = { version = "0.2.1", optional = true } strip-ansi-escapes = { version = "0.2.1", optional = true }
serde_json = "1"
[dependencies.ratatui] [dependencies.ratatui]
version = "0.30" version = "0.30"

View File

@ -13,6 +13,7 @@ use crate::{
}; };
pub mod html; pub mod html;
pub mod search;
#[derive(Clone)] #[derive(Clone)]
pub struct CliWebConfig { pub struct CliWebConfig {
@ -58,6 +59,7 @@ pub fn generate_web(all_packages: &Vec<String>, config: &CliWebConfig) {
let mut valid_packages = Vec::new(); let mut valid_packages = Vec::new();
let mut dependents_map: HashMap<String, BTreeSet<String>> = HashMap::new(); let mut dependents_map: HashMap<String, BTreeSet<String>> = HashMap::new();
let mut files_map: BTreeMap<String, String> = BTreeMap::new();
for package_name in all_packages { for package_name in all_packages {
let Ok(package_name) = PackageName::new(package_name) else { let Ok(package_name) = PackageName::new(package_name) else {
@ -108,6 +110,10 @@ pub fn generate_web(all_packages: &Vec<String>, config: &CliWebConfig) {
&html_path, &html_path,
&config, &config,
); );
if let Some(file_map) = stage_files {
files_map.insert(package.name.to_string(), file_map);
}
} }
let mut grouped_packages: BTreeMap<String, Vec<&(Package, CookRecipe)>> = BTreeMap::new(); let mut grouped_packages: BTreeMap<String, Vec<&(Package, CookRecipe)>> = BTreeMap::new();
@ -120,7 +126,16 @@ pub fn generate_web(all_packages: &Vec<String>, config: &CliWebConfig) {
let index_path = repo_path.join("index.html"); let index_path = repo_path.join("index.html");
let style_path = repo_path.join("style.css"); let style_path = repo_path.join("style.css");
generate_html_index(grouped_packages, &index_path, &config); 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_path = repo_path.join("files.json");
index_files
.write(&files_path)
.expect("Failed to write files.json");
} }
pub(crate) fn get_category(dir: &Path) -> String { pub(crate) fn get_category(dir: &Path) -> String {

113
src/web/search.rs Normal file
View File

@ -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<String, FsNode>),
File(String),
}
pub struct FileIndexBuilder {
inner: BTreeMap<String, FsNode>,
}
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<String> = 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(())
}
}