use std::path::PathBuf;
use anyhow::Result;
use iroh::{EndpointAddr, EndpointId};
use iroh_blobs::Hash;
use crate::core::peers::PeerStore;
pub fn default_data_dir() -> PathBuf {
dirs_next::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".ringdrop")
}
pub fn parse_peer_id(s: &str) -> Result<EndpointId> {
s.parse()
.map_err(|e| anyhow::anyhow!("invalid peer id: {e}"))
}
pub(crate) fn relay_only_addr(full: EndpointAddr) -> EndpointAddr {
full.relay_urls()
.fold(EndpointAddr::new(full.id), |a, url| {
a.with_relay_url(url.clone())
})
}
pub(crate) fn format_peer_entry(peer: &EndpointId, nick: Option<&str>) -> String {
match nick {
Some(n) => format!("{peer} ({n})"),
None => peer.to_string(),
}
}
pub(crate) fn display_peer(peer: &EndpointId, store: &PeerStore) -> String {
let nick = store.get(peer).ok().flatten().flatten();
format_peer_entry(peer, nick.as_deref())
}
pub fn print_banner() {
use std::io::IsTerminal as _;
const GIRAFFE: &str = concat!(
" . .l'. . . \n",
" . . . ..lO'.. .\n",
" .,lod.. .... \n",
" . ..ooO:.....Oood... . \n",
". . ....,0xoodxlllk... .. \n",
" . ....cdlollllllooo0..... . ..\n",
" ..,dlll . oK . oKKK:.. \n",
" ..clolll__l,NX__lloll0.. . . \n",
" ...0olollooloooxKKKKooxlO.. .\n",
" .ooloooKKKKXlooooXKxllKKKx.. . \n",
"..llxllollkKkOloKKKKKolXKKl.. \n",
"..xddXkkkkO0olloKXXdlllloK:.. . . . \n",
"..olkkkNKoKKKlx'....ddKK0l:. .. \n",
" .cxdoll0:....... .lllodoc. ... . \n",
" ......... . . .cK0oKKl.. ..... \n",
" . .,KKkKKc.. ...... \n",
" . .:loxolc........... .\n",
" . . ..odKKl:.. ....... \n",
" . .. ..KXlKKO.. ....... \n",
" ..'KKdlXo.. ....... .\n",
);
const RINGDROP: &str = concat!(
" _ _ \n",
" (_) | | \n",
" _ __ _ _ __ __ _ __| |_ __ ___ _ __ \n",
"| '__| | '_ \\ / _` |/ _` | '__/ _ \\| '_ \\ \n",
"| | | | | | | (_| | (_| | | | (_) | |_) |\n",
"|_| |_|_| |_|\\__, |\\__,_|_| \\___/| .__/ \n",
" __/ | | | \n",
" |___/ |_| ",
);
const TEXT_START: usize = 6;
const GIRAFFE_COLS: usize = 38;
const GAP: &str = "";
const YELLOW: &str = "\x1b[93m";
const RESET: &str = "\x1b[0m";
let version = env!("CARGO_PKG_VERSION");
let colored = std::io::stdout().is_terminal()
&& std::env::var_os("NO_COLOR").is_none()
&& std::env::var("TERM").as_deref() != Ok("dumb");
let giraffe_lines: Vec<&str> = GIRAFFE.lines().collect();
let ringdrop_lines: Vec<&str> = RINGDROP.lines().collect();
for (i, giraffe_line) in giraffe_lines.iter().enumerate() {
let giraffe_col = giraffe_line.len().min(GIRAFFE_COLS);
let giraffe_display = &giraffe_line[..giraffe_col];
let text = i
.checked_sub(TEXT_START)
.and_then(|j| ringdrop_lines.get(j));
if colored {
let padded = format!("{:<GIRAFFE_COLS$}", giraffe_display.trim_end());
match text {
Some(t) => println!("{YELLOW}{padded}{RESET}{GAP}{}", rainbow_line(t)),
None => println!("{YELLOW}{giraffe_display}{RESET}"),
}
} else {
match text {
Some(t) => println!("{:<GIRAFFE_COLS$}{GAP}{t}", giraffe_display.trim_end()),
None => println!("{giraffe_display}"),
}
}
}
if colored {
println!("\n {YELLOW}v{version}{RESET}\n");
} else {
println!("\n v{version}\n");
}
}
fn rainbow_line(line: &str) -> String {
const PALETTE: &[u8] = &[
196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 48, 50, 51, 45, 39, 33, 27, 21, 57,
93, 129, 165, 201,
];
let mut out = String::new();
let mut active_color: Option<u8> = None;
for (col, ch) in line.chars().enumerate() {
if ch == ' ' {
out.push(' ');
} else {
let color = PALETTE[col % PALETTE.len()];
if active_color != Some(color) {
out.push_str(&format!("\x1b[38;5;{color}m"));
active_color = Some(color);
}
out.push(ch);
}
}
if active_color.is_some() {
out.push_str("\x1b[0m");
}
out
}
pub fn parse_hash(s: &str) -> Result<Hash> {
s.parse().map_err(|e| anyhow::anyhow!("invalid hash: {e}"))
}
#[cfg(test)]
mod tests {
use iroh::SecretKey;
use iroh_blobs::Hash;
use super::*;
#[test]
fn parse_peer_id_accepts_valid_key_string() {
let id = SecretKey::generate().public();
let s = id.to_string();
assert_eq!(parse_peer_id(&s).unwrap(), id);
}
#[test]
fn parse_peer_id_rejects_garbage() {
let err = parse_peer_id("not-a-valid-peer-id").unwrap_err();
assert!(err.to_string().contains("invalid peer id"));
}
#[test]
fn parse_hash_accepts_valid_hex() {
let hash = Hash::from_bytes([0x42; 32]);
let hex = hash.to_string();
assert_eq!(parse_hash(&hex).unwrap(), hash);
}
#[test]
fn parse_hash_rejects_invalid_hex_chars() {
let err = parse_hash(&"z".repeat(64)).unwrap_err();
assert!(err.to_string().contains("invalid hash"));
}
#[test]
fn relay_only_addr_preserves_node_id_when_no_relay() {
let id = SecretKey::generate().public();
let addr = EndpointAddr::new(id);
let result = relay_only_addr(addr);
assert_eq!(result.id, id);
}
#[test]
fn relay_only_addr_preserves_relay_url() {
use iroh::RelayUrl;
let id = SecretKey::generate().public();
let url: RelayUrl = "https://relay.example.com".parse().unwrap();
let addr = EndpointAddr::new(id).with_relay_url(url.clone());
let result = relay_only_addr(addr);
assert_eq!(result.id, id);
assert!(result.relay_urls().any(|u| u == &url));
}
#[test]
fn format_peer_entry_without_nickname_returns_raw_id() {
let id = SecretKey::generate().public();
let result = format_peer_entry(&id, None);
assert_eq!(result, id.to_string());
}
#[test]
fn format_peer_entry_with_nickname_includes_nickname_in_parens() {
let id = SecretKey::generate().public();
let result = format_peer_entry(&id, Some("alice"));
assert!(result.contains(&id.to_string()));
assert!(result.contains("(alice)"));
}
}