1
use chrono::{Duration, TimeDelta};
2

            
3
use crate::proto::finger_protocol::{
4
    FingerResponseStructuredUserEntry, FingerResponseUserSession, MailStatus,
5
};
6

            
7
pub 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

            
91
4
fn format_session_for_finger(session: &FingerResponseUserSession, max_tty_len: usize) -> String {
92
4
    let mut result = String::new();
93

            
94
4
    result += &format!(
95
4
        "On since {} on {} from {}",
96
4
        session.login_time.format("%a %b %e %H:%M (%Z)"),
97
4
        session.tty,
98
4
        session.host.as_deref().unwrap_or("unknown")
99
4
    );
100

            
101
4
    if let Some(idle_time) = session.idle_time {
102
4
        result += &format!(
103
4
            "\n{:width$}{} idle",
104
4
            "",
105
4
            format_idle_time_for_finger(idle_time),
106
4
            width = max_tty_len - session.tty.len() + 3,
107
4
        );
108
4
    }
109

            
110
4
    if !session.messages_on {
111
1
        result += "\n     (messages off)";
112
3
    }
113

            
114
4
    result
115
4
}
116

            
117
4
fn format_idle_time_for_finger(idle_time: TimeDelta) -> String {
118
4
    debug_assert!(
119
        idle_time.num_seconds() >= 0,
120
        "Idle time should never be negative"
121
    );
122

            
123
4
    let mut result = String::new();
124

            
125
4
    let days = idle_time.num_days();
126
4
    let hours = (idle_time - Duration::days(days)).num_hours();
127
4
    let minutes = (idle_time - Duration::days(days) - Duration::hours(hours)).num_minutes();
128
4
    let seconds =
129
4
        (idle_time - Duration::days(days) - Duration::hours(hours) - Duration::minutes(minutes))
130
4
            .num_seconds();
131

            
132
4
    if days > 0 {
133
1
        result += &format!("{} day{} ", days, if days == 1 { "" } else { "s" });
134
3
    }
135
4
    if hours > 0 {
136
3
        result += &format!("{} hour{} ", hours, if hours == 1 { "" } else { "s" });
137
1
    }
138
4
    if minutes > 0 && days == 0 {
139
3
        result += &format!("{} minute{} ", minutes, if minutes == 1 { "" } else { "s" });
140
1
    }
141
4
    if seconds > 0 && days == 0 && hours == 0 {
142
1
        result += &format!("{} second{} ", seconds, if seconds == 1 { "" } else { "s" });
143
3
    }
144

            
145
4
    result.trim().to_string()
146
4
}
147

            
148
#[cfg(test)]
149
mod 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
1
    fn test_format_session_for_finger() {
159
1
        let values = [
160
1
            (
161
1
                FingerResponseUserSession {
162
1
                    tty: "pts/14".to_string(),
163
1
                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
164
1
                    host: Some(":pts/21:S.5".to_string()),
165
1
                    idle_time: Some(Duration::days(3) + Duration::hours(18)),
166
1
                    messages_on: true,
167
1
                },
168
1
                indoc! {
169
1
                  "
170
1
                  On since Tue May 12 15:39 (UTC) on pts/14 from :pts/21:S.5
171
1
                     3 days 18 hours idle
172
1
                "
173
1
                }
174
1
                .trim()
175
1
                .to_string(),
176
1
            ),
177
1
            (
178
1
                FingerResponseUserSession {
179
1
                    tty: "pts/16".to_string(),
180
1
                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
181
1
                    host: Some(":pts/21:S.6".to_string()),
182
1
                    idle_time: Some(Duration::hours(18) + Duration::minutes(47)),
183
1
                    messages_on: true,
184
1
                },
185
1
                indoc! {
186
1
                "
187
1
                  On since Tue May 12 15:39 (UTC) on pts/16 from :pts/21:S.6
188
1
                     18 hours 47 minutes idle
189
1
                "
190
1
                }
191
1
                .trim()
192
1
                .to_string(),
193
1
            ),
194
1
            (
195
1
                FingerResponseUserSession {
196
1
                    tty: "pts/21".to_string(),
197
1
                    login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
198
1
                    host: Some("212.102.29.193".to_string()),
199
1
                    idle_time: Some(Duration::minutes(24) + Duration::seconds(22)),
200
1
                    messages_on: false,
201
1
                },
202
1
                indoc! {
203
1
                "
204
1
                  On since Tue May 12 15:39 (UTC) on pts/21 from 212.102.29.193
205
1
                     24 minutes 22 seconds idle
206
1
                       (messages off)
207
1
                "
208
1
                }
209
1
                .trim()
210
1
                .to_string(),
211
1
            ),
212
1
        ];
213

            
214
3
        for (session, expected) in values {
215
3
            assert_eq!(
216
3
                format_session_for_finger(&session, "pts/10".len()),
217
                expected
218
            );
219
        }
220
1
    }
221

            
222
    #[test]
223
1
    fn test_format_session_for_finger_long_tty_string() {
224
1
        let session = FingerResponseUserSession {
225
1
            tty: "pts/1".to_string(),
226
1
            login_time: Utc.with_ymd_and_hms(2026, 5, 12, 15, 39, 0).unwrap(),
227
1
            host: Some(":pts/21:S.5".to_string()),
228
1
            idle_time: Some(Duration::hours(1) + Duration::minutes(30)),
229
1
            messages_on: true,
230
1
        };
231

            
232
1
        let expected = indoc! {
233
1
            "
234
1
              On since Tue May 12 15:39 (UTC) on pts/1 from :pts/21:S.5
235
1
                   1 hour 30 minutes idle
236
1
            "
237
1
        }
238
1
        .trim()
239
1
        .to_string();
240

            
241
1
        assert_eq!(
242
1
            format_session_for_finger(&session, "pts/123".len()),
243
            expected
244
        );
245
1
    }
246
}