1use crate::cli::Output;
7use crate::error::Result;
8use crate::storage::SessionStore;
9use std::collections::HashMap;
10
11pub fn list_tags(store: &SessionStore, json: bool, output: &Output) -> Result<()> {
25 let ids = store.list()?;
26
27 let mut tag_counts: HashMap<String, usize> = HashMap::new();
29 for id in &ids {
30 if let Ok((header, _footer)) = store.load_header_and_footer(id) {
31 for tag in &header.tags {
32 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
33 }
34 }
35 }
36
37 let mut sorted_tags: Vec<(String, usize)> = tag_counts.into_iter().collect();
39 sorted_tags.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
40
41 if sorted_tags.is_empty() {
43 if json {
44 println!("[]");
45 } else {
46 output.info("No tags found. Add tags with: rec tag <session> <tag>");
47 }
48 return Ok(());
49 }
50
51 if json {
53 let json_entries: Vec<serde_json::Value> = sorted_tags
54 .iter()
55 .map(|(tag, count)| {
56 serde_json::json!({
57 "tag": tag,
58 "count": count,
59 })
60 })
61 .collect();
62
63 println!(
64 "{}",
65 serde_json::to_string_pretty(&json_entries).unwrap_or_else(|_| "[]".to_string())
66 );
67 return Ok(());
68 }
69
70 println!();
72 for (tag, count) in &sorted_tags {
73 let label = if *count == 1 { "session" } else { "sessions" };
74 println!(" {tag:<20} ({count} {label})");
75 }
76 println!();
77
78 Ok(())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::models::{Command, Session, SessionStatus};
85 use crate::storage::Paths;
86 use std::path::PathBuf;
87 use tempfile::TempDir;
88
89 fn create_test_paths(temp_dir: &TempDir) -> Paths {
90 Paths {
91 data_dir: temp_dir.path().join("sessions"),
92 config_dir: temp_dir.path().join("config"),
93 config_file: temp_dir.path().join("config").join("config.toml"),
94 state_dir: temp_dir.path().join("state"),
95 }
96 }
97
98 fn create_tagged_session(name: &str, tags: Vec<String>) -> Session {
99 let mut session = Session::new(name);
100 session.header.tags = tags;
101 session.commands.push(Command::new(
102 0,
103 "echo hello".to_string(),
104 PathBuf::from("/tmp"),
105 ));
106 session.complete(SessionStatus::Completed);
107 session
108 }
109
110 #[test]
111 fn test_tag_counting() {
112 let temp_dir = TempDir::new().unwrap();
113 let paths = create_test_paths(&temp_dir);
114 let store = SessionStore::new(paths);
115
116 let s1 = create_tagged_session("s1", vec!["deploy".into(), "rust".into()]);
118 let s2 = create_tagged_session("s2", vec!["deploy".into(), "setup".into()]);
119 let s3 = create_tagged_session("s3", vec!["rust".into()]);
120 store.save(&s1).unwrap();
121 store.save(&s2).unwrap();
122 store.save(&s3).unwrap();
123
124 let ids = store.list().unwrap();
126 let mut tag_counts: HashMap<String, usize> = HashMap::new();
127 for id in &ids {
128 if let Ok((header, _)) = store.load_header_and_footer(id) {
129 for tag in &header.tags {
130 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
131 }
132 }
133 }
134
135 assert_eq!(tag_counts.get("deploy"), Some(&2));
136 assert_eq!(tag_counts.get("rust"), Some(&2));
137 assert_eq!(tag_counts.get("setup"), Some(&1));
138 }
139
140 #[test]
141 fn test_empty_tags() {
142 let temp_dir = TempDir::new().unwrap();
143 let paths = create_test_paths(&temp_dir);
144 let store = SessionStore::new(paths);
145
146 let s = create_tagged_session("no-tags", vec![]);
148 store.save(&s).unwrap();
149
150 let ids = store.list().unwrap();
151 let mut tag_counts: HashMap<String, usize> = HashMap::new();
152 for id in &ids {
153 if let Ok((header, _)) = store.load_header_and_footer(id) {
154 for tag in &header.tags {
155 *tag_counts.entry(tag.clone()).or_insert(0) += 1;
156 }
157 }
158 }
159
160 assert!(tag_counts.is_empty());
161 }
162}