wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-vault/src/utils.rs@main

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1use std::fs::File;
2
3use camino::Utf8Path;
4use jiff::Timestamp;
5
6#[cfg(feature = "git")]
7static CREATED: &str = r#"git log --reverse -1 --pretty="format:%cI" --"#;
8#[cfg(feature = "git")]
9static MODIFIED: &str = r#"git log -1 --pretty="format:%cI" --"#;
10
11#[cfg(feature = "git")]
12fn run(arg: &str) -> Result<Option<Timestamp>, Error> {
13    use std::process::Command;
14
15    if which::which("git").is_err() {
16        return Ok(None);
17    }
18
19    let (shell, cmd) = if cfg!(target_os = "windows") {
20        ("cmd", "/C")
21    } else {
22        ("sh", "-s")
23    };
24
25    tracing::debug!(cmd=?arg, "running git command");
26
27    let output = Command::new(shell)
28        .arg(cmd)
29        .arg(arg)
30        .output()
31        .map_err(Error::Command)?;
32
33    if !output.status.success() {
34        return Err(Error::CommandFailed(
35            String::from_utf8_lossy(&output.stdout).to_string(),
36        ));
37    }
38
39    let output = String::from_utf8(output.stdout).map_err(Error::OutputUtf8)?;
40    let output = output.trim();
41
42    // TODO: should this be an error, it catches git the file doesnt have an associated commit
43    if output.is_empty() || output.starts_with("fatal") {
44        return Ok(None);
45    }
46
47    let timestamp = output.parse().map_err(Error::TimestampParse)?;
48
49    Ok(Some(timestamp))
50}
51
52#[derive(Debug, miette::Diagnostic, thiserror::Error)]
53pub enum Error {
54    #[error("failed to open file")]
55    FileOpen(#[source] std::io::Error),
56    #[error("failed to get metadata of file")]
57    FileMetadata(#[source] std::io::Error),
58    #[error("failed to get timestamp of file")]
59    FileTime(#[source] std::io::Error),
60    #[error("failed to convert timestamp from system time of file")]
61    TimestampConvert(#[source] jiff::Error),
62
63    #[cfg(feature = "git")]
64    #[error("failed to run command against file")]
65    Command(#[source] std::io::Error),
66    #[cfg(feature = "git")]
67    #[error("failed to run command against file: {0}")]
68    CommandFailed(String),
69    #[cfg(feature = "git")]
70    #[error("failed to convert command output to utf-8 for file")]
71    OutputUtf8(#[source] std::string::FromUtf8Error),
72    #[cfg(feature = "git")]
73    #[error("failed to parse timestamp of file")]
74    TimestampParse(#[source] jiff::Error),
75}
76
77/// Metadata of an arbitrary Vault file.
78///
79/// Currently only stores the file's creation and modification times.
80#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
81pub struct FileMetadata {
82    /// The date the file was created according to the file system.
83    ///
84    /// This may not be correct.
85    pub created: Timestamp,
86    /// The date the file was modified according to the file system.
87    ///
88    /// This may not be correct.
89    pub modified: Timestamp,
90
91    /// The date the file was created according to Git's commit history.
92    pub git_created: Option<Timestamp>,
93    /// The date the file was created modified to Git's commit history.
94    pub git_modified: Option<Timestamp>,
95}
96
97impl FileMetadata {
98    pub fn new(path: &Utf8Path) -> Result<Self, Error> {
99        let file = File::open(path.as_std_path()).map_err(Error::FileOpen)?;
100        let meta = file.metadata().map_err(Error::FileMetadata)?;
101
102        let created = meta.created().map_err(Error::FileTime)?;
103        let created = Timestamp::try_from(created).map_err(Error::TimestampConvert)?;
104
105        let modified = meta.modified().map_err(Error::FileTime)?;
106        let modified = Timestamp::try_from(modified).map_err(Error::TimestampConvert)?;
107
108        #[cfg(not(feature = "git"))]
109        let git_created = None;
110        #[cfg(not(feature = "git"))]
111        let git_modified = None;
112
113        #[cfg(feature = "git")]
114        let git_created = run(&format!("{CREATED} {path}"))?;
115        #[cfg(feature = "git")]
116        let git_modified = run(&format!("{MODIFIED} {path}"))?;
117
118        Ok(Self {
119            created,
120            modified,
121
122            git_created,
123            git_modified,
124        })
125    }
126}
127
128#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
129pub struct Heading {
130    pub level: usize,
131    pub id: String,
132    pub title: String,
133    pub children: Vec<Self>,
134}
135
136pub fn extract_headings(contents: &str) -> Vec<Heading> {
137    let mut headings = Vec::new();
138    let mut in_code_block = false;
139
140    for line in contents.lines() {
141        if line.trim_start().starts_with("```") {
142            in_code_block = !in_code_block;
143
144            continue;
145        }
146
147        if in_code_block {
148            continue;
149        }
150
151        if line.starts_with('#') {
152            let level = line.chars().take_while(|&c| c == '#').count();
153            let title = line[level..].trim().to_string();
154
155            headings.push(Heading {
156                level,
157                id: String::new(),
158                title,
159                children: vec![],
160            });
161        }
162    }
163
164    headings
165}
166
167// Header code taken from Zola
168/// Converts the flat temp headings into a nested set of headings
169/// representing the hierarchy
170pub fn make_table_of_contents(headings: Vec<Heading>) -> Vec<Heading> {
171    let mut toc = vec![];
172    for heading in headings {
173        // First heading or we try to insert the current heading in a previous one
174        if toc.is_empty() || !insert_into_parent(toc.iter_mut().last(), &heading) {
175            toc.push(heading);
176        }
177    }
178
179    toc
180}
181
182// Takes a potential (mutable) parent and a heading to try and insert into
183// Returns true when it performed the insertion, false otherwise
184fn insert_into_parent(potential_parent: Option<&mut Heading>, heading: &Heading) -> bool {
185    match potential_parent {
186        None => {
187            // No potential parent to insert into so it needs to be insert higher
188            false
189        }
190        Some(parent) => {
191            if heading.level <= parent.level {
192                // Heading is same level or higher so we don't insert here
193                return false;
194            }
195            if heading.level + 1 == parent.level {
196                // We have a direct child of the parent
197                parent.children.push(heading.clone());
198                return true;
199            }
200            // We need to go deeper
201            if !insert_into_parent(parent.children.iter_mut().last(), heading) {
202                // No, we need to insert it here
203                parent.children.push(heading.clone());
204            }
205            true
206        }
207    }
208}
209