Skip to main content

roowho2_lib/proto/finger_protocol/
classic_formatter.rs

1use chrono::{Duration, TimeDelta};
2
3use crate::proto::finger_protocol::{
4    FingerResponseStructuredUserEntry, FingerResponseUserSession, MailStatus,
5};
6
7pub fn classic_format_finger_response_structured_user_entry(
8    entry: &FingerResponseStructuredUserEntry,
9) -> String {
10    let mut result = String::new();
11
12    result += &format!(
13        "Login: {:<16}\t\t\tName: {}\n",
14        entry.username, entry.full_name
15    );
16    result += &format!(
17        "Directory: {:<24}\tShell: {}\n",
18        entry.home_dir.display(),
19        entry.shell.display()
20    );
21
22    if let Some(office) = &entry.office {
23        result += &format!("Office: {}\n", office);
24    }
25    if let Some(office_phone) = &entry.office_phone {
26        result += &format!("Office Phone: {}\n", office_phone);
27    }
28    if let Some(home_phone) = &entry.home_phone {
29        result += &format!("Home Phone: {}\n", home_phone);
30    }
31
32    if entry.never_logged_in {
33        result += "Never logged in.\n";
34    } else {
35        let max_tty_len = entry
36            .sessions
37            .iter()
38            .map(|s| s.tty.len())
39            .max()
40            .unwrap_or(0);
41
42        for session in &entry.sessions {
43            result += &format_session_for_finger(session, max_tty_len);
44            result += "\n";
45        }
46    }
47
48    if let Some(forward) = &entry.forward_status {
49        result += &format!("Mail forwarded to {}\n", forward);
50    }
51
52    if let Some(mail_status) = &entry.mail_status {
53        match mail_status {
54            MailStatus::NoMail => result += "No mail.\n",
55            MailStatus::NewMailReceived {
56                received_time,
57                unread_since,
58            } => {
59                result += &format!(
60                    "New mail received {}\nUnread since {}\n",
61                    received_time.format("%a %b %e %H:%M (%Z)"),
62                    unread_since.format("%a %b %e %H:%M (%Z)")
63                );
64            }
65            MailStatus::MailLastRead(last_read) => {
66                result += &format!(
67                    "Mail last read {}\n",
68                    last_read.format("%a %b %e %H:%M (%Z)")
69                );
70            }
71        }
72    }
73
74    if let Some(pgp_key) = &entry.pgp_key {
75        result += &format!("PGP key:\n{}\n", pgp_key);
76    }
77
78    if let Some(project) = &entry.project {
79        result += &format!("Project:\n{}\n", project);
80    }
81
82    if let Some(plan) = &entry.plan {
83        result += &format!("Plan:\n{}\n", plan);
84    } else {
85        result += "No Plan.\n";
86    }
87
88    result.trim().to_string()
89}
90
91fn format_session_for_finger(session: &FingerResponseUserSession, max_tty_len: usize) -> String {
92    let mut result = String::new();
93
94    result += &format!(
95        "On since {} on {} from {}",
96        session.login_time.format("%a %b %e %H:%M (%Z)"),
97        session.tty,
98        session.host.as_deref().unwrap_or("unknown")
99    );
100
101    if let Some(idle_time) = session.idle_time {
102        result += &format!(
103            "\n{:width$}{} idle",
104            "",
105            format_idle_time_for_finger(idle_time),
106            width = max_tty_len - session.tty.len() + 3,
107        );
108    }
109
110    if !session.messages_on {
111        result += "\n     (messages off)";
112    }
113
114    result
115}
116
117fn format_idle_time_for_finger(idle_time: TimeDelta) -> String {
118    debug_assert!(
119        idle_time.num_seconds() >= 0,
120        "Idle time should never be negative"
121    );
122
123    let mut result = String::new();
124
125    let days = idle_time.num_days();
126    let hours = (idle_time - Duration::days(days)).num_hours();
127    let minutes = (idle_time - Duration::days(days) - Duration::hours(hours)).num_minutes();
128    let seconds =
129        (idle_time - Duration::days(days) - Duration::hours(hours) - Duration::minutes(minutes))
130            .num_seconds();
131
132    if days > 0 {
133        result += &format!("{} day{} ", days, if days == 1 { "" } else { "s" });
134    }
135    if hours > 0 {
136        result += &format!("{} hour{} ", hours, if hours == 1 { "" } else { "s" });
137    }
138    if minutes > 0 && days == 0 {
139        result += &format!("{} minute{} ", minutes, if minutes == 1 { "" } else { "s" });
140    }
141    if seconds > 0 && days == 0 && hours == 0 {
142        result += &format!("{} second{} ", seconds, if seconds == 1 { "" } else { "s" });
143    }
144
145    result.trim().to_string()
146}
147
148#[cfg(test)]
149mod tests {
150    use chrono::{TimeZone, Utc};
151    use indoc::indoc;
152
153    use crate::proto::finger_protocol::FingerResponseUserSession;
154
155    use super::*;
156
157    #[test]
158    fn test_format_session_for_finger() {
159        let values = [
160            (
161                FingerResponseUserSession {
162                    tty: "pts/14".to_string(),
163                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
164                    host: Some(":pts/21:S.5".to_string()),
165                    idle_time: Some(Duration::days(3) + Duration::hours(18)),
166                    messages_on: true,
167                },
168                indoc! {
169                  "
170                  On since Tue May 12 15:39 (UTC) on pts/14 from :pts/21:S.5
171                     3 days 18 hours idle
172                "
173                }
174                .trim()
175                .to_string(),
176            ),
177            (
178                FingerResponseUserSession {
179                    tty: "pts/16".to_string(),
180                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
181                    host: Some(":pts/21:S.6".to_string()),
182                    idle_time: Some(Duration::hours(18) + Duration::minutes(47)),
183                    messages_on: true,
184                },
185                indoc! {
186                "
187                  On since Tue May 12 15:39 (UTC) on pts/16 from :pts/21:S.6
188                     18 hours 47 minutes idle
189                "
190                }
191                .trim()
192                .to_string(),
193            ),
194            (
195                FingerResponseUserSession {
196                    tty: "pts/21".to_string(),
197                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
198                    host: Some("212.102.29.193".to_string()),
199                    idle_time: Some(Duration::minutes(24) + Duration::seconds(22)),
200                    messages_on: false,
201                },
202                indoc! {
203                "
204                  On since Tue May 12 15:39 (UTC) on pts/21 from 212.102.29.193
205                     24 minutes 22 seconds idle
206                       (messages off)
207                "
208                }
209                .trim()
210                .to_string(),
211            ),
212        ];
213
214        for (session, expected) in values {
215            assert_eq!(
216                format_session_for_finger(&session, "pts/10".len()),
217                expected
218            );
219        }
220    }
221
222    #[test]
223    fn test_format_session_for_finger_long_tty_string() {
224        let session = FingerResponseUserSession {
225            tty: "pts/1".to_string(),
226            login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
227            host: Some(":pts/21:S.5".to_string()),
228            idle_time: Some(Duration::hours(1) + Duration::minutes(30)),
229            messages_on: true,
230        };
231
232        let expected = indoc! {
233            "
234              On since Tue May 12 15:39 (UTC) on pts/1 from :pts/21:S.5
235                   1 hour 30 minutes idle
236            "
237        }
238        .trim()
239        .to_string();
240
241        assert_eq!(
242            format_session_for_finger(&session, "pts/123".len()),
243            expected
244        );
245    }
246}