1use clap::Args;
2use inquire::{Confirm, Select};
3use itertools::Itertools;
4use lux_lib::{
5 config::Config,
6 lockfile::LocalPackage,
7 lua_rockspec::RemoteLuaRockspec,
8 lua_version::LuaVersion,
9 package::PackageReq,
10 rockspec::Rockspec,
11 tree::{InstallTree, RockMatches, Tree},
12};
13use miette::{miette, Context, IntoDiagnostic, Result};
14use url::Url;
15use walkdir::WalkDir;
16
17#[derive(Args)]
18pub struct Doc {
19 package: PackageReq,
20
21 #[arg(long)]
23 online: bool,
24}
25
26pub async fn doc(args: Doc, config: Config) -> Result<()> {
27 let tree = config.user_tree(LuaVersion::from(&config)?.clone())?;
28 let package_id = match tree.match_rocks(&args.package)? {
29 RockMatches::NotFound(package_req) => {
30 Err(miette!("No package matching {} found.", package_req))
31 }
32 RockMatches::Many(_package_ids) => Err(miette!(
33 "
34Found multiple packages matching {}.
35Please specify an exact package (<name>@<version>) or narrow the version requirement.
36",
37 &args.package
38 )),
39 RockMatches::Single(package_id) => Ok(package_id),
40 }?;
41 let lockfile = tree.lockfile()?;
42 let pkg = lockfile
43 .get(&package_id)
44 .ok_or_else(|| miette!("package is installed, but not found in the lockfile"))?
45 .clone();
46 if args.online {
47 open_homepage(pkg, &tree).await
48 } else {
49 open_local_docs(pkg, &tree, &config).await
50 }
51}
52
53async fn open_homepage(pkg: LocalPackage, tree: &Tree) -> Result<()> {
54 let homepage = match get_homepage(&pkg, tree)? {
55 Some(homepage) => Ok(homepage),
56 None => Err(miette!(
57 "Package {} does not have a homepage in its RockSpec.",
58 pkg.into_package_spec()
59 )),
60 }?;
61 open::that(homepage.to_string()).into_diagnostic()?;
62 Ok(())
63}
64
65fn get_homepage(pkg: &LocalPackage, tree: &Tree) -> Result<Option<Url>> {
66 let layout = tree.installed_rock_layout(pkg)?;
67 let rockspec_content = std::fs::read_to_string(layout.rockspec_path()).into_diagnostic()?;
68 let rockspec = RemoteLuaRockspec::new(&rockspec_content)?;
69 Ok(rockspec.description().homepage.clone())
70}
71
72async fn open_local_docs(pkg: LocalPackage, tree: &Tree, config: &Config) -> Result<()> {
73 let layout = tree.installed_rock_layout(&pkg)?;
74 let files: Vec<String> = WalkDir::new(&layout.doc)
75 .into_iter()
76 .filter_map_ok(|file| {
77 let path = file.into_path();
78 if path.is_file() {
79 path.file_name()
80 .map(|file_name| file_name.to_string_lossy().to_string())
81 } else {
82 None
83 }
84 })
85 .try_collect()
86 .into_diagnostic()?;
87 match files.first() {
88 Some(file) if files.len() == 1 => {
89 edit::edit_file(layout.doc.join(file)).into_diagnostic()?;
90 Ok(())
91 }
92 Some(_) => {
93 let file = Select::new(
94 "Multiple documentation files found. Please select one to open.",
95 files,
96 )
97 .prompt()
98 .into_diagnostic()
99 .wrap_err("error selecting from multiple files")?;
100 edit::edit_file(layout.doc.join(file)).into_diagnostic()?;
101 Ok(())
102 }
103 None => match get_homepage(&pkg, tree)? {
104 None => Err(miette!(
105 "No documentation found for package {}",
106 pkg.into_package_spec()
107 )),
108 Some(homepage) => {
109 if config.no_prompt() {
110 return Err(miette!(
111 "No local documentation found for package {}",
112 pkg.into_package_spec()
113 ));
114 } else if Confirm::new("No local documentation found. Open homepage?")
115 .with_default(false)
116 .prompt()
117 .into_diagnostic()
118 .wrap_err("error prompting to open homepage")?
119 {
120 open::that(homepage.to_string()).into_diagnostic()?;
121 }
122 Ok(())
123 }
124 },
125 }
126}