umshctl/command/
routes_cmd.rs

1//! `routes`: what this tool has learned about reaching other nodes.
2//!
3//! Reads the cache file rather than a radio, so it answers with nothing
4//! attached — which is the state you are in when you want to know why
5//! last night's `manage` took the path it did.
6
7use anyhow::Result;
8
9use crate::command::{format_duration, values::KeyArg};
10use crate::output::{field, note, subfield};
11use crate::routes::{ROUTE_TTL, RouteCache};
12
13#[derive(Debug, clap::Subcommand)]
14pub enum RoutesOp {
15    /// The route remembered for one node.
16    Show {
17        #[arg(value_name = "KEY")]
18        key: KeyArg,
19    },
20    /// Forget remembered routes, all of them or one node's.
21    ///
22    /// The next command to that node rediscovers the path, so this costs
23    /// one flood and is the right answer when a repeater has moved.
24    Clear {
25        #[arg(value_name = "KEY")]
26        key: Option<KeyArg>,
27    },
28}
29
30pub fn run(op: Option<RoutesOp>) -> Result<()> {
31    let mut cache = RouteCache::load();
32    match op {
33        None => list(&cache),
34        Some(RoutesOp::Show { key }) => show(&cache, umsh::core::PublicKey(key.0)),
35        Some(RoutesOp::Clear { key: Some(key) }) => {
36            let key = umsh::core::PublicKey(key.0);
37            if cache.remove(&key) {
38                println!("forgot the route to {key}");
39            } else {
40                println!("no route was remembered for {key}");
41            }
42            cache.store()
43        }
44        Some(RoutesOp::Clear { key: None }) => {
45            match cache.clear() {
46                0 => println!("no routes were remembered"),
47                1 => println!("forgot 1 route"),
48                count => println!("forgot {count} routes"),
49            }
50            cache.store()
51        }
52    }
53}
54
55fn list(cache: &RouteCache) -> Result<()> {
56    if cache.is_empty() {
57        println!("no routes remembered");
58        note("a route is learned from a reply, and remembered for a day");
59        return Ok(());
60    }
61    // A key is 44 characters, which is wider than any value column, so
62    // it heads its own entry rather than sharing a line with one.
63    for (key, record) in cache.iter() {
64        println!("{key}");
65        subfield("route", crate::routes::describe(&record.route));
66        subfield("learned", ago(record.age()));
67        subfield("expires", within(record.age()));
68    }
69    Ok(())
70}
71
72fn show(cache: &RouteCache, key: umsh::core::PublicKey) -> Result<()> {
73    let Some(record) = cache.get(&key) else {
74        println!("no route remembered for {key}");
75        note("the next command to it discovers one, and this remembers it");
76        return Ok(());
77    };
78    println!("{key}");
79    field("route", crate::routes::describe(&record.route));
80    field("learned", ago(record.age()));
81    field("expires", within(record.age()));
82    Ok(())
83}
84
85fn ago(age: std::time::Duration) -> String {
86    format!(
87        "{} ago",
88        format_duration(age.as_secs().min(u32::MAX.into()) as u32)
89    )
90}
91
92fn within(age: std::time::Duration) -> String {
93    match ROUTE_TTL.checked_sub(age) {
94        Some(left) => format!("in {}", format_duration(left.as_secs() as u32)),
95        None => "now".to_string(),
96    }
97}