Skip to main content

lux_cli/
doc.rs

1use clap::Args;
2use eyre::{eyre, Context, OptionExt, Result};
3use inquire::{Confirm, Select};
4use itertools::Itertools;
5use lux_lib::{
6    config::Config,
7    lockfile::LocalPackage,
8    lua_rockspec::RemoteLuaRockspec,
9    lua_version::LuaVersion,
10    package::PackageReq,
11    rockspec::Rockspec,
12    tree::{RockMatches, Tree},
13};
14use url::Url;
15use walkdir::WalkDir;
16
17#[derive(Args)]
18pub struct Doc {
19    package: PackageReq,
20
21    /// Ignore local docs and open the package's homepage in a browser.
22    #[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(eyre!("No package matching {} found.", package_req))
31        }
32        RockMatches::Many(_package_ids) => Err(eyre!(
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_eyre("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).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(eyre!(
57            "Package {} does not have a homepage in its RockSpec.",
58            pkg.into_package_spec()
59        )),
60    }?;
61    open::that(homepage.to_string())?;
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())?;
68    let rockspec = RemoteLuaRockspec::new(&rockspec_content)?;
69    Ok(rockspec.description().homepage.clone())
70}
71
72async fn open_local_docs(pkg: LocalPackage, tree: &Tree) -> 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    match files.first() {
87        Some(file) if files.len() == 1 => {
88            edit::edit_file(layout.doc.join(file))?;
89            Ok(())
90        }
91        Some(_) => {
92            let file = Select::new(
93                "Multiple documentation files found. Please select one to open.",
94                files,
95            )
96            .prompt()
97            .wrap_err("error selecting from multiple files")?;
98            edit::edit_file(layout.doc.join(file))?;
99            Ok(())
100        }
101        None => match get_homepage(&pkg, tree)? {
102            None => Err(eyre!(
103                "No documentation found for package {}",
104                pkg.into_package_spec()
105            )),
106            Some(homepage) => {
107                if Confirm::new("No local documentation found. Open homepage?")
108                    .with_default(false)
109                    .prompt()
110                    .wrap_err("error prompting to open homepage")?
111                {
112                    open::that(homepage.to_string())?;
113                }
114                Ok(())
115            }
116        },
117    }
118}