lux_cli/
doc.rs

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