Skip to main content

hennery_kernel/
recents.rs

1//! Project recents (kernel spec §1.1, §5.3; plan 6c decision 11): the
2//! directories sessions started or resumed in, per host and hat, newest
3//! first. They share the host registry's connection and owner, and every
4//! query names the owner (kernel spec §1).
5
6use crate::hats::{host_default_hat, is_canonical, resolve, rules_of};
7use crate::hosts::{Hosts, is_displayable_path};
8use anyhow::Result;
9use rusqlite::{TransactionBehavior, params};
10use std::collections::HashSet;
11
12/// The recents kept per host and hat; older ones are dropped.
13pub const KEPT: usize = 50;
14
15/// The recents the project picker shows.
16pub const SHOWN: usize = 20;
17
18/// One recent project.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Recent {
21    pub path: String,
22    /// Seconds since the Unix epoch.
23    pub last_used_at: i64,
24}
25
26impl Hosts {
27    /// Remember that a session of `hat_id` started or resumed in `path` on
28    /// `host_id` at `now`, keeping the newest `KEPT` of that host and hat.
29    /// `false`, and nothing remembered, for a path that is not canonical or
30    /// cannot be shown: until the host resolves a session's cwd (hats 5b),
31    /// it is what its client sent.
32    pub fn remember(&self, host_id: &str, hat_id: &str, path: &str, now: i64) -> Result<bool> {
33        if !is_canonical(path) || !is_displayable_path(path) {
34            return Ok(false);
35        }
36        let owner = self.owner_id();
37        let mut conn = self.conn();
38        let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
39        tx.execute(
40            "INSERT INTO project_recents(owner_id, host_id, hat_id, path, last_used_at) VALUES (?1, ?2, ?3, ?4, ?5)
41             ON CONFLICT(host_id, hat_id, path) DO UPDATE SET last_used_at = max(last_used_at, excluded.last_used_at)
42                 WHERE project_recents.owner_id = ?1",
43            params![owner, host_id, hat_id, path, now],
44        )?;
45        tx.execute(
46            "DELETE FROM project_recents WHERE owner_id = ?1 AND host_id = ?2 AND hat_id = ?3 AND path NOT IN (
47                 SELECT path FROM project_recents WHERE owner_id = ?1 AND host_id = ?2 AND hat_id = ?3
48                 ORDER BY last_used_at DESC, path LIMIT ?4)",
49            params![owner, host_id, hat_id, KEPT as i64],
50        )?;
51        tx.commit()?;
52        Ok(true)
53    }
54
55    /// The newest `limit` recents of `host_id` whose path resolves to
56    /// `hat_id` now (kernel spec §5.3; the review's A1): resolved against
57    /// the host's current rules and default hat, not the hat stored when
58    /// each was remembered, which a rule or a default changed since leaves
59    /// stale. Empty for an unknown host.
60    pub fn recents(&self, host_id: &str, hat_id: &str, limit: usize) -> Result<Vec<Recent>> {
61        // One read of the rules, the default hat and the recents, under one
62        // lock, as `resolve_hat` reads (Task 6's review).
63        let conn = self.conn();
64        let Some(default_hat) = host_default_hat(&conn, self.owner_id(), host_id)? else {
65            return Ok(Vec::new());
66        };
67        let rules = rules_of(&conn, self.owner_id(), host_id)?;
68        let mut stmt = conn.prepare(
69            "SELECT path, last_used_at FROM project_recents WHERE owner_id = ?1 AND host_id = ?2
70             ORDER BY last_used_at DESC, path",
71        )?;
72        let rows = stmt.query_map(params![self.owner_id(), host_id], |r| {
73            Ok(Recent {
74                path: r.get(0)?,
75                last_used_at: r.get(1)?,
76            })
77        })?;
78        let mut seen = HashSet::new();
79        let mut shown = Vec::new();
80        for recent in rows {
81            let recent = recent?;
82            // Newest first, so a path stored under two hats is shown once,
83            // with its latest use.
84            if resolve(&rules, &default_hat, &recent.path).hat_id == hat_id && seen.insert(recent.path.clone()) {
85                shown.push(recent);
86                if shown.len() == limit {
87                    break;
88                }
89            }
90        }
91        Ok(shown)
92    }
93}