#[path = "./lib.rs"]
mod lib;
#[derive(clap::Parser)]
#[clap(
author,
version,
about,
long_about = None
)]
pub struct Options {
#[clap(long)]
all: bool,
#[clap(long)]
list: bool,
#[clap(long)]
locate: bool,
#[clap(long, env = "HOST", default_value = "127.0.0.1")]
host: String,
#[clap(short = 'p', long, env = "PORT", default_value = "8080")]
port: String,
#[clap(short = 'r', long, env = "CARGO_BOOK_RANDOM_PORT")]
random_port: bool,
#[clap(short = 's', long, name = "ITEM")]
search: Option<String>,
#[clap(short = 'o', long, env = "CARGO_BOOK_OPEN")]
open: bool,
book: Option<Book>,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
clap::Parser,
strum::EnumIter,
strum::EnumString,
strum::Display,
strum::EnumMessage,
)]
enum Book {
#[strum(serialize = "book")]
Rust,
#[strum(serialize = "rust-by-example")]
RustByExample,
#[strum(serialize = "rustlings")]
Rustlings,
#[strum(serialize = "std")]
Std,
#[strum(serialize = "edition-guide")]
EditionGuide,
#[strum(serialize = "rustc")]
Rustc,
#[strum(serialize = "cargo")]
Cargo,
#[strum(serialize = "rustdoc")]
Rustdoc,
#[strum(serialize = "reference")]
Reference,
#[strum(serialize = "nomicon")]
Nomicon,
#[strum(serialize = "unstable-book")]
UnstableBook,
#[strum(serialize = "rust-dev-guide")]
RustDevGuide,
#[strum(serialize = "embedded-book")]
EmbeddedBook,
}
struct Row(Book);
impl ToString for Row {
fn to_string(&self) -> String {
use strum::EnumMessage;
format!("{: <16} {}", self.0, self.0.get_documentation().unwrap()).into()
}
}
impl Options {
fn host(&self) -> String {
self.host.clone()
}
fn port(&self) -> String {
self.port.clone()
}
fn get_port(&self) -> std::io::Result<u16> {
Ok(port_selector::random_free_tcp_port().expect("Error allocating free port"))
}
fn hostport(&self) -> String {
format!("{}:{}", self.host(), self.port())
}
fn url(&self) -> String {
format!("http://{}", self.hostport())
}
fn book_link(&self) -> String {
if let Some(ref book) = self.book {
if *book == Book::Rustlings {
return "https://github.com/rust-lang/rustlings".to_string();
}
if *book == Book::RustDevGuide {
return "https://rustc-dev-guide.rust-lang.org".to_string();
}
if self.search.is_none() {
format!("{}/{book}", self.url())
} else {
format!(
"{}/{book}/?search={}",
self.url(),
self.search.as_ref().unwrap()
)
}
} else {
self.link()
}
}
fn link(&self) -> String {
if self.search.is_none() {
format!("{}", self.url())
} else {
format!(
"{}/std/?search={}",
self.url(),
self.search.as_ref().unwrap()
)
}
}
fn addr(&self) -> std::net::SocketAddr {
self.hostport().parse().unwrap()
}
fn open(&self) -> Result<(), anyhow::Error> {
Ok(if self.open {
let link = self.book_link();
log::info!("Opening {link}");
self.open_browser(link)?
})
}
fn open_browser<P: AsRef<std::ffi::OsStr>>(&self, path: P) -> Result<(), anyhow::Error> {
Ok(opener::open_browser(path)?)
}
pub async fn run(&mut self) -> Result<(), anyhow::Error> {
if self.random_port {
self.port = format!("{}", self.get_port()?);
}
Ok(if self.locate {
let dir = lib::find_rustdoc()
.unwrap()
.into_os_string()
.into_string()
.unwrap();
println!("{dir}")
} else if self.list {
use strum::EnumMessage;
use strum::IntoEnumIterator;
for book in Book::iter() {
println!("{: <16} {}", book, book.get_documentation().unwrap());
}
} else {
if self.book.is_none() && !self.all {
use dialoguer::console::Term;
use dialoguer::{theme::ColorfulTheme, Select};
use strum::IntoEnumIterator;
let books: Vec<Row> = Book::iter().map(|x| Row(x)).collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.items(&books)
.default(0)
.interact_on_opt(&Term::stderr())?;
match selection {
Some(index) => self.book = Some(books[index].0),
None => return Ok(()),
}
}
log::info!("Serving rust doc on {}", &self.url());
self.open()?;
lib::serve_rustbook(&self.addr()).await?
})
}
}