Skip to main content

limit_cli/
project_settings.rs

1//! Project-specific settings storage
2//!
3//! Manages per-project settings in SQLite (tracking.db).
4
5use crate::error::CliError;
6use rusqlite::{params, Connection};
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9use std::path::PathBuf;
10
11pub struct ProjectSettings {
12    db_path: PathBuf,
13}
14
15impl ProjectSettings {
16    pub fn new() -> Result<Self, CliError> {
17        let home_dir = dirs::home_dir()
18            .ok_or_else(|| CliError::ConfigError("Failed to get home directory".to_string()))?;
19        let limit_dir = home_dir.join(".limit");
20        std::fs::create_dir_all(&limit_dir).map_err(|e| {
21            CliError::ConfigError(format!("Failed to create .limit directory: {}", e))
22        })?;
23
24        let db_path = limit_dir.join("tracking.db");
25        let settings = Self { db_path };
26        settings.init_db()?;
27        Ok(settings)
28    }
29
30    fn init_db(&self) -> Result<(), CliError> {
31        let conn = Connection::open(&self.db_path)
32            .map_err(|e| CliError::ConfigError(format!("Failed to open database: {}", e)))?;
33
34        conn.execute(
35            "CREATE TABLE IF NOT EXISTS project_settings (
36                project_hash TEXT PRIMARY KEY,
37                warm_enabled INTEGER NOT NULL DEFAULT 0,
38                warmed_at TEXT
39            )",
40            [],
41        )
42        .map_err(|e| CliError::ConfigError(format!("Failed to create table: {}", e)))?;
43
44        Ok(())
45    }
46
47    /// Compute a unique hash for a project path
48    pub fn hash_project(project_path: &std::path::Path) -> String {
49        let canonical = project_path
50            .canonicalize()
51            .unwrap_or_else(|_| project_path.to_path_buf());
52
53        let mut hasher = DefaultHasher::new();
54        canonical.to_string_lossy().hash(&mut hasher);
55        format!("{:x}", hasher.finish())
56    }
57
58    /// Check if warm is enabled for a project
59    pub fn is_warm_enabled(&self, project_path: &std::path::Path) -> bool {
60        let hash = Self::hash_project(project_path);
61        let conn = match Connection::open(&self.db_path) {
62            Ok(c) => c,
63            Err(_) => return false,
64        };
65
66        let enabled: i64 = conn
67            .query_row(
68                "SELECT warm_enabled FROM project_settings WHERE project_hash = ?",
69                params![&hash],
70                |row| row.get(0),
71            )
72            .unwrap_or(0);
73
74        enabled != 0
75    }
76
77    /// Enable warm for a project
78    pub fn set_warm_enabled(&self, project_path: &std::path::Path) -> Result<(), CliError> {
79        let hash = Self::hash_project(project_path);
80        let conn = Connection::open(&self.db_path)
81            .map_err(|e| CliError::ConfigError(format!("Failed to open database: {}", e)))?;
82
83        let now = chrono::Utc::now().to_rfc3339();
84
85        conn.execute(
86            "INSERT OR REPLACE INTO project_settings (project_hash, warm_enabled, warmed_at) VALUES (?1, 1, ?2)",
87            params![&hash, &now],
88        )
89        .map_err(|e| CliError::ConfigError(format!("Failed to update settings: {}", e)))?;
90
91        Ok(())
92    }
93}
94
95impl Default for ProjectSettings {
96    fn default() -> Self {
97        Self::new().expect("Failed to create ProjectSettings")
98    }
99}