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 {package_req} found"))
31 }
32 RockMatches::Many(_package_ids) => Err(miette!(
33 help = "specify an exact package (<name>@<version>) or narrow the version requirement",
34 "found multiple packages matching {}",
35 &args.package
36 )),
37 RockMatches::Single(package_id) => Ok(package_id),
38 }?;
39 let lockfile = tree.lockfile()?;
40 let pkg = lockfile
41 .get(&package_id)
42 .ok_or_else(|| miette!("package is installed, but not found in the lockfile"))?
43 .clone();
44 if args.online {
45 open_homepage(pkg, &tree).await
46 } else {
47 open_local_docs(pkg, &tree, &config).await
48 }
49}
50
51async fn open_homepage(pkg: LocalPackage, tree: &Tree) -> Result<()> {
52 let homepage = match get_homepage(&pkg, tree)? {
53 Some(homepage) => Ok(homepage),
54 None => Err(miette!(
55 "package {} does not have a homepage in its RockSpec",
56 pkg.into_package_spec()
57 )),
58 }?;
59 open::that(homepage.to_string()).into_diagnostic()?;
60 Ok(())
61}
62
63fn get_homepage(pkg: &LocalPackage, tree: &Tree) -> Result<Option<Url>> {
64 let layout = tree.installed_rock_layout(pkg)?;
65 let rockspec_content = std::fs::read_to_string(layout.rockspec_path()).into_diagnostic()?;
66 let rockspec = RemoteLuaRockspec::new(&rockspec_content)?;
67 Ok(rockspec.description().homepage.clone())
68}
69
70async fn open_local_docs(pkg: LocalPackage, tree: &Tree, config: &Config) -> Result<()> {
71 let layout = tree.installed_rock_layout(&pkg)?;
72 let files: Vec<String> = WalkDir::new(&layout.doc)
73 .into_iter()
74 .filter_map_ok(|file| {
75 let path = file.into_path();
76 if path.is_file() {
77 path.file_name()
78 .map(|file_name| file_name.to_string_lossy().to_string())
79 } else {
80 None
81 }
82 })
83 .try_collect()
84 .into_diagnostic()?;
85 match files.first() {
86 Some(file) if files.len() == 1 => {
87 edit::edit_file(layout.doc.join(file)).into_diagnostic()?;
88 Ok(())
89 }
90 Some(_) => {
91 let file = Select::new(
92 "Multiple documentation files found. Please select one to open.",
93 files,
94 )
95 .prompt()
96 .into_diagnostic()
97 .wrap_err("error selecting from multiple files")?;
98 edit::edit_file(layout.doc.join(file)).into_diagnostic()?;
99 Ok(())
100 }
101 None => match get_homepage(&pkg, tree)? {
102 None => Err(miette!(
103 "no documentation found for package '{}'",
104 pkg.into_package_spec()
105 )),
106 Some(homepage) => {
107 if config.no_prompt() {
108 return Err(miette!(
109 "no local documentation found for package '{}'",
110 pkg.into_package_spec()
111 ));
112 } else if Confirm::new("No local documentation found. Open homepage?")
113 .with_default(false)
114 .prompt()
115 .into_diagnostic()
116 .wrap_err("error prompting to open homepage")?
117 {
118 open::that(homepage.to_string()).into_diagnostic()?;
119 }
120 Ok(())
121 }
122 },
123 }
124}