use std::{
fmt::Display,
io::{stdout, Write},
};
use ansi_term::{Colour, Style};
use clap::Parser;
use url::Url;
use crate::{
core::run,
danger,
media::{Gemini, Line, Media, Preformat},
network::TcpNetwork,
Ui,
};
#[derive(Parser)]
pub struct App {
#[arg(short, long)]
pub test_mode: bool,
pub url: Option<String>,
}
pub enum Error {}
impl App {
pub fn run(self) -> Result<(), Error> {
let mut cli = Cli {};
if self.test_mode {
test_run(&mut cli);
Ok(())
} else {
let url = self.url.unwrap_or_else(|| {
"gemini://gemini.circumlunar.space/docs/specification.gmi".into()
});
let url: Url = url.parse().expect("Not a valid URL");
let mut net = TcpNetwork::new(danger::Naive);
run(&mut net, &mut cli, url);
Ok(())
}
}
}
fn test_run(cli: &mut Cli) {
let text: Gemini = "plain line
# heading1
## heading 2
### heading 3
> quote 1
> quote 2
> quote 3
line
line 2
line 3
* list 1
* list 2
* list 3
=> gemini://url?query description
```rs
some rust code here
match true {
true
=> false,
false
=> true,
}
```
# heading
> quote
line
* list
=> gemini://url description
ordinary line"
.into();
let secret = cli.read_secret("tell me your secrets").unwrap();
cli.warn(format_args!("your secret was: {secret}"));
let overt = cli
.read("tell me something that isn't secret, since I can't be trusted with secrets")
.unwrap();
cli.warn(format_args!("your response was: {overt}"));
cli.render_gemini(text);
}
pub struct Cli {}
impl Cli {
fn render_gemini(&self, g: Gemini) {
let Gemini { lines } = g;
for line in lines {
match line {
Line::Heading { level, title } => {
let title = title.as_ref();
let char = match level {
crate::media::HeadingLevel::L1 => '=',
crate::media::HeadingLevel::L2 => '-',
crate::media::HeadingLevel::L3 => ' ',
};
let under = core::iter::repeat(char)
.take(title.len())
.collect::<String>();
let style = ansi_term::Style::new().bold();
println!();
println!("{}", style.paint(&under));
println!("{}", style.paint(title));
println!("{}", style.paint(&under));
}
Line::Link { url, description } => {
let blue = Colour::Cyan;
print!("{}: {}", '🔗', blue.paint(url));
if let Some(description) = description {
let italic = ansi_term::Style::new().italic();
println!(" | {}", italic.paint(description));
}
}
Line::Text(t) => println!("{}", t),
Line::Preformatted(p) => {
let Preformat { alt, lines } = p;
let dim = Style::new().dimmed().italic();
let bg = Style::new().on(Colour::Black);
println!("{}", dim.paint(alt));
for line in lines {
println!("{}", bg.paint(line));
}
println!();
}
Line::ListItem(li) => {
println!(" • {}", li)
}
Line::Quote(q) => {
let italic = Style::new().italic();
println!(" | {}", italic.paint(q));
}
}
}
}
}
impl Ui for Cli {
fn show(&mut self, content: Media) {
match content {
Media::Gemini(g) => self.render_gemini(g),
Media::Text(s) => println!("{s}"),
}
}
fn read(&mut self, prompt: impl Display) -> Option<String> {
print!("{prompt}: ");
stdout().flush().unwrap();
Some(
std::io::stdin()
.lines()
.next()?
.expect("Failed to read input"),
)
}
fn read_secret(&mut self, prompt: impl Display) -> Option<String> {
let dim = ansi_term::Style::new().dimmed();
print!("{prompt}: ");
let hidden = "(input hidden)";
let len = hidden.len() as u32;
let back = ansi_control_codes::control_sequences::CUB(Some(len));
print!("{}{back}", dim.paint(hidden));
stdout().flush().unwrap();
match rpassword::read_password() {
Ok(v) => Some(v),
Err(e) => match e.kind() {
std::io::ErrorKind::UnexpectedEof => None,
_ => panic!("Failed to read input"),
},
}
}
fn warn<D: std::fmt::Display>(&mut self, warning: D) {
let red = Colour::Red;
println!("{}: {}", red.paint("Warning"), warning)
}
}