use std::net::SocketAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use guppy_protocol::{FetchOptions, FileHandler, GuppyResponse, ServerConfig, fetch, serve};
const USAGE: &str = "guppy — the Guppy protocol (spec: github.com/dimkr/guppy-protocol, v0.4.4)
USAGE:
guppy fetch <guppy://url>
Fetch a URL and print the body. Attach input as the URL's query
(percent-encoded) when a prompt asks for it.
guppy serve --root DIR [--listen ADDR:PORT]
Serve a directory (index.gmi per directory, gemtext-first).
Default listen address is 0.0.0.0:6775.
";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(args) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("{message}");
ExitCode::FAILURE
}
}
}
fn run(args: Vec<String>) -> Result<(), String> {
let Some((command, rest)) = args.split_first() else {
return Err(USAGE.to_string());
};
let mut positional = Vec::new();
let mut root: Option<PathBuf> = None;
let mut listen: Option<String> = None;
let mut iter = rest.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--root" => root = Some(PathBuf::from(iter.next().ok_or("--root needs a value")?)),
"--listen" => listen = Some(iter.next().ok_or("--listen needs a value")?.clone()),
other if other.starts_with("--") => return Err(format!("Unknown flag {other}")),
other => positional.push(other.to_string()),
}
}
match command.as_str() {
"fetch" => {
let [url] = positional.as_slice() else {
return Err("usage: guppy fetch <guppy://url>".into());
};
let response = block_on(fetch(url, &FetchOptions::default()))?;
match response {
GuppyResponse::Success { mime, body } => {
eprintln!("success: {mime}");
use std::io::Write;
std::io::stdout()
.write_all(&body)
.map_err(|error| error.to_string())
}
GuppyResponse::Prompt { text } => {
println!("input required: {text}");
println!("(repeat with the input as the URL query, e.g. ?your%20answer)");
Ok(())
}
GuppyResponse::Redirect { target } => {
println!("redirect: {target}");
Ok(())
}
GuppyResponse::Error { message } => Err(format!("error: {message}")),
}
}
"serve" => {
let root = root.ok_or("guppy serve: --root DIR is required")?;
let listen: SocketAddr = listen
.as_deref()
.unwrap_or("0.0.0.0:6775")
.parse()
.map_err(|_| "guppy serve: --listen needs ADDR:PORT".to_string())?;
block_on(async move {
let socket = tokio::net::UdpSocket::bind(listen)
.await
.map_err(|error| format!("bind {listen}: {error}"))?;
eprintln!("guppy: serving {} on {listen} (ctrl-c to stop)", root.display());
serve(
socket,
FileHandler::new(root),
ServerConfig::default(),
async {
let _ = tokio::signal::ctrl_c().await;
},
)
.await
.map_err(|error| error.to_string())
})
}
"--help" | "-h" | "help" => {
println!("{USAGE}");
Ok(())
}
other => Err(format!("Unknown command '{other}'.\n\n{USAGE}")),
}
}
fn block_on<T, E: ToString>(
future: impl std::future::Future<Output = Result<T, E>>,
) -> Result<T, String> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| error.to_string())?
.block_on(future)
.map_err(|error| error.to_string())
}