#[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 std::fmt::Display for Row {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use strum::EnumMessage;
write!(f, "{: <16} {}", self.0, self.0.get_documentation().unwrap())
}
}
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)?)
}
fn generate_index_html(&self) -> String {
use strum::EnumMessage;
use strum::IntoEnumIterator;
let base = self.url();
let rows: String = Book::iter()
.map(|book| {
let desc = book.get_documentation().unwrap_or_default();
let href = match book {
Book::Rustlings => {
"https://github.com/rust-lang/rustlings".to_string()
}
Book::RustDevGuide => {
"https://rustc-dev-guide.rust-lang.org".to_string()
}
_ => format!("{base}/{book}/"),
};
format!(
r#"<tr><td><a href="{href}">{book}</a></td><td>{desc}</td></tr>"#,
href = href,
book = book,
desc = desc,
)
})
.collect::<Vec<_>>()
.join("\n");
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rust Books Index</title>
<style>
body {{ font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }}
h1 {{ border-bottom: 1px solid #ccc; padding-bottom: .5rem; }}
table {{ border-collapse: collapse; width: 100%; }}
td, th {{ text-align: left; padding: .4rem .8rem; border-bottom: 1px solid #eee; }}
th {{ background: #f5f5f5; }}
a {{ color: #0074d9; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h1>Rust Books</h1>
<table>
<thead><tr><th>Book</th><th>Description</th></tr></thead>
<tbody>
{rows}
</tbody>
</table>
</body>
</html>
"#,
rows = rows,
)
}
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.all {
let index_html = self.generate_index_html();
log::info!("Serving all rust books on {}", &self.url());
self.open()?;
lib::serve_rustbook_with_index(&self.addr(), index_html).await?
} else {
if self.book.is_none() {
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?
})
}
}