codex_mobile_bridge/storage/
directories.rs1use std::path::Path;
2
3use rusqlite::{OptionalExtension, params};
4
5use super::Storage;
6use crate::bridge_protocol::{DirectoryBookmarkRecord, DirectoryHistoryRecord, now_millis};
7use crate::directory::{
8 canonicalize_directory, default_display_name, normalize_absolute_directory,
9};
10
11impl Storage {
12 pub fn list_directory_bookmarks(&self) -> anyhow::Result<Vec<DirectoryBookmarkRecord>> {
13 let conn = self.connect()?;
14 let mut stmt = conn.prepare(
15 "SELECT path, display_name, created_at_ms, updated_at_ms
16 FROM directory_bookmarks
17 ORDER BY display_name COLLATE NOCASE ASC",
18 )?;
19
20 let rows = stmt.query_map([], |row| {
21 Ok(DirectoryBookmarkRecord {
22 path: row.get(0)?,
23 display_name: row.get(1)?,
24 created_at_ms: row.get(2)?,
25 updated_at_ms: row.get(3)?,
26 })
27 })?;
28
29 Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
30 }
31
32 pub fn upsert_directory_bookmark(
33 &self,
34 path: &Path,
35 display_name: Option<&str>,
36 ) -> anyhow::Result<DirectoryBookmarkRecord> {
37 let canonical = canonicalize_directory(path)?;
38 let path_string = canonical.to_string_lossy().to_string();
39 let now = now_millis();
40 let conn = self.connect()?;
41 let existing = conn
42 .query_row(
43 "SELECT created_at_ms FROM directory_bookmarks WHERE path = ?1",
44 params![path_string],
45 |row| row.get::<_, i64>(0),
46 )
47 .optional()?;
48 let created_at_ms = existing.unwrap_or(now);
49 let display_name = display_name
50 .map(str::trim)
51 .filter(|value| !value.is_empty())
52 .map(ToOwned::to_owned)
53 .unwrap_or_else(|| default_display_name(&canonical));
54 conn.execute(
55 "INSERT INTO directory_bookmarks (path, display_name, created_at_ms, updated_at_ms)
56 VALUES (?1, ?2, ?3, ?4)
57 ON CONFLICT(path) DO UPDATE SET
58 display_name = excluded.display_name,
59 updated_at_ms = excluded.updated_at_ms",
60 params![path_string, display_name, created_at_ms, now],
61 )?;
62
63 Ok(DirectoryBookmarkRecord {
64 path: canonical.to_string_lossy().to_string(),
65 display_name,
66 created_at_ms,
67 updated_at_ms: now,
68 })
69 }
70
71 pub fn remove_directory_bookmark(&self, path: &Path) -> anyhow::Result<()> {
72 let canonical = normalize_absolute_directory(path)?;
73 let conn = self.connect()?;
74 conn.execute(
75 "DELETE FROM directory_bookmarks WHERE path = ?1",
76 params![canonical.to_string_lossy().to_string()],
77 )?;
78 Ok(())
79 }
80
81 pub fn list_directory_history(
82 &self,
83 limit: usize,
84 ) -> anyhow::Result<Vec<DirectoryHistoryRecord>> {
85 let conn = self.connect()?;
86 let mut stmt = conn.prepare(
87 "SELECT path, last_used_at_ms, use_count
88 FROM directory_history
89 ORDER BY last_used_at_ms DESC
90 LIMIT ?1",
91 )?;
92 let rows = stmt.query_map(params![limit.max(1) as i64], |row| {
93 let path: String = row.get(0)?;
94 Ok(DirectoryHistoryRecord {
95 display_name: default_display_name(Path::new(&path)),
96 path,
97 last_used_at_ms: row.get(1)?,
98 use_count: row.get(2)?,
99 })
100 })?;
101 Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
102 }
103
104 pub fn record_directory_usage(&self, path: &Path) -> anyhow::Result<DirectoryHistoryRecord> {
105 let normalized = normalize_absolute_directory(path)?;
106 let path_string = normalized.to_string_lossy().to_string();
107 let now = now_millis();
108 let conn = self.connect()?;
109 let existing = conn
110 .query_row(
111 "SELECT use_count FROM directory_history WHERE path = ?1",
112 params![&path_string],
113 |row| row.get::<_, i64>(0),
114 )
115 .optional()?
116 .unwrap_or(0);
117 let use_count = existing + 1;
118 conn.execute(
119 "INSERT INTO directory_history (path, last_used_at_ms, use_count)
120 VALUES (?1, ?2, ?3)
121 ON CONFLICT(path) DO UPDATE SET
122 last_used_at_ms = excluded.last_used_at_ms,
123 use_count = directory_history.use_count + 1",
124 params![path_string, now, use_count],
125 )?;
126 Ok(DirectoryHistoryRecord {
127 path: normalized.to_string_lossy().to_string(),
128 display_name: default_display_name(&normalized),
129 last_used_at_ms: now,
130 use_count,
131 })
132 }
133}