Skip to main content

dial/learning/
mod.rs

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    // Validate category
12    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}