1use crate::db::get_db;
2use crate::errors::{DialError, Result};
3use crate::output::{blue, bold, dim, print_success, yellow};
4use rusqlite::Connection;
5
6pub const LEARNING_CATEGORIES: &[&str] = &["build", "test", "setup", "gotcha", "pattern", "tool", "other"];
7
8pub fn add_learning(description: &str, category: Option<&str>) -> Result<i64> {
9 let conn = get_db(None)?;
10
11 let category = match category {
13 Some(c) if LEARNING_CATEGORIES.contains(&c) => Some(c),
14 Some(c) => {
15 println!("{}", yellow(&format!("Warning: Unknown category '{}'. Using 'other'.", c)));
16 Some("other")
17 }
18 None => None,
19 };
20
21 conn.execute(
22 "INSERT INTO learnings (category, description) VALUES (?1, ?2)",
23 rusqlite::params![category, description],
24 )?;
25
26 let learning_id = conn.last_insert_rowid();
27 let cat_str = category.map(|c| format!(" [{}]", c)).unwrap_or_default();
28
29 let preview = if description.len() > 60 {
30 format!("{}...", &description[..60])
31 } else {
32 description.to_string()
33 };
34
35 print_success(&format!("Added learning #{}{}: {}", learning_id, cat_str, preview));
36 Ok(learning_id)
37}
38
39pub fn list_learnings(category: Option<&str>) -> Result<()> {
40 let conn = get_db(None)?;
41
42 let rows: Vec<(i64, Option<String>, String, String, i64)> = if let Some(cat) = category {
43 let mut stmt = conn.prepare(
44 "SELECT id, category, description, discovered_at, times_referenced
45 FROM learnings WHERE category = ?1
46 ORDER BY discovered_at DESC",
47 )?;
48 let result = stmt.query_map([cat], |row| {
49 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?))
50 })?
51 .collect::<std::result::Result<Vec<_>, _>>()?;
52 result
53 } else {
54 let mut stmt = conn.prepare(
55 "SELECT id, category, description, discovered_at, times_referenced
56 FROM learnings ORDER BY discovered_at DESC",
57 )?;
58 let result = stmt.query_map([], |row| {
59 Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?))
60 })?
61 .collect::<std::result::Result<Vec<_>, _>>()?;
62 result
63 };
64
65 if rows.is_empty() {
66 println!("{}", dim("No learnings recorded."));
67 return Ok(());
68 }
69
70 let title = if let Some(cat) = category {
71 format!("Learnings ({})", cat)
72 } else {
73 "Learnings".to_string()
74 };
75
76 println!("{}", bold(&title));
77 println!("{}", "=".repeat(60));
78
79 for (id, cat, description, discovered_at, times_referenced) in rows {
80 let cat_str = cat
81 .map(|c| format!("[{}]", c))
82 .unwrap_or_else(|| "[uncategorized]".to_string());
83
84 let ref_str = if times_referenced > 0 {
85 format!("(referenced {}x)", times_referenced)
86 } else {
87 String::new()
88 };
89
90 println!("\n #{} {} {}", id, blue(&cat_str), ref_str);
91 println!(" {}", description);
92 println!("{}", dim(&format!(" Discovered: {}", &discovered_at[..10])));
93 }
94
95 Ok(())
96}
97
98pub fn search_learnings(query: &str) -> Result<Vec<LearningResult>> {
99 let conn = get_db(None)?;
100
101 let mut stmt = conn.prepare(
102 "SELECT l.id, l.category, l.description, l.times_referenced
103 FROM learnings l
104 INNER JOIN learnings_fts fts ON l.id = fts.rowid
105 WHERE learnings_fts MATCH ?1
106 ORDER BY rank LIMIT 10",
107 )?;
108
109 let rows: Vec<LearningResult> = stmt
110 .query_map([query], |row| {
111 Ok(LearningResult {
112 id: row.get(0)?,
113 category: row.get(1)?,
114 description: row.get(2)?,
115 times_referenced: row.get(3)?,
116 })
117 })?
118 .collect::<std::result::Result<Vec<_>, _>>()?;
119
120 if rows.is_empty() {
121 println!("{}", dim(&format!("No learnings matching '{}'.", query)));
122 return Ok(rows);
123 }
124
125 println!("{}", bold(&format!("Learnings matching '{}':", query)));
126 println!("{}", "=".repeat(60));
127
128 for row in &rows {
129 let cat_str = row
130 .category
131 .as_ref()
132 .map(|c| format!("[{}]", c))
133 .unwrap_or_default();
134 println!("\n #{} {}", row.id, blue(&cat_str));
135 println!(" {}", row.description);
136 }
137
138 Ok(rows)
139}
140
141#[derive(Debug, Clone)]
142pub struct LearningResult {
143 pub id: i64,
144 pub category: Option<String>,
145 pub description: String,
146 pub times_referenced: i64,
147}
148
149pub fn delete_learning(learning_id: i64) -> Result<()> {
150 let conn = get_db(None)?;
151
152 let changed = conn.execute("DELETE FROM learnings WHERE id = ?1", [learning_id])?;
153
154 if changed == 0 {
155 return Err(DialError::LearningNotFound(learning_id));
156 }
157
158 print_success(&format!("Deleted learning #{}.", learning_id));
159 Ok(())
160}
161
162pub fn increment_learning_reference(conn: &Connection, learning_id: i64) -> Result<()> {
163 conn.execute(
164 "UPDATE learnings SET times_referenced = times_referenced + 1 WHERE id = ?1",
165 [learning_id],
166 )?;
167 Ok(())
168}