Skip to main content

very_normal/
charts.rs

1//! A charts page: one self-contained HTML file built from your progress, with
2//! nothing fetched from the network, so it opens anywhere and works offline.
3
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use serde_json::json;
8
9use crate::store::Progress;
10use crate::{coach, gate, layout};
11
12const TEMPLATE: &str = include_str!("charts.html");
13const DATA_MARK: &str = "\"__VERY_NORMAL_DATA__\"";
14
15/// Next to the progress file: `~/.local/share/very-normal/charts.html`.
16pub fn default_path(progress: &Path) -> PathBuf {
17    progress.with_file_name("charts.html")
18}
19
20/// The page, with your progress, the plan's stages and the coach's and gate's
21/// numbers baked in.
22pub fn html(progress: &Progress, now_ms: u64) -> String {
23    let stages: Vec<_> = (0..=layout::last())
24        .map(|s| {
25            json!({
26                "name": layout::name(s),
27                "keys": layout::keys_string(s),
28                "focus": layout::focus(s).iter().collect::<String>(),
29            })
30        })
31        .collect();
32    let data = json!({
33        "generated": now_ms,
34        "plan": layout::plan().name,
35        "stages": stages,
36        "baseline": progress.baseline(),
37        "progress": progress,
38        "goal": coach::DAILY_GOAL,
39        "breakMinutes": coach::BREAK,
40        "gate": {
41            "wordsAccuracy": gate::WORDS_ACCURACY,
42            "speedShare": gate::SPEED_SHARE,
43            "realAccuracy": gate::REAL_ACCURACY,
44        },
45    });
46    // `</` inside a script would end it early
47    let data = data.to_string().replace("</", "<\\/");
48    TEMPLATE.replacen(DATA_MARK, &data, 1)
49}
50
51pub fn write(progress: &Progress, path: &Path, now_ms: u64) -> Result<()> {
52    if let Some(dir) = path.parent() {
53        std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
54    }
55    std::fs::write(path, html(progress, now_ms))
56        .with_context(|| format!("writing {}", path.display()))
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn the_data_lands_in_the_page_and_cannot_close_the_script() {
65        let mut p = Progress::default();
66        p.manual_mut(0).wpm = 33.0;
67        p.warmed_up = Some("</script><b>".into());
68        let page = html(&p, 1);
69        assert!(!page.contains(DATA_MARK));
70        assert!(page.contains("\"baseline\":33.0"));
71        assert!(!page.contains("</script><b>"));
72        assert_eq!(page.matches("</script>").count(), 1);
73    }
74}