cliboard 0.1.0

Live math whiteboard for your terminal — render LaTeX equations in the browser with per-step AI chat
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
    pub title: String,
    pub theme: Theme,
    pub blocks: Vec<Block>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
    #[default]
    Dark,
    Light,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Block {
    Step {
        id: usize,
        title: String,
        equations: Vec<String>,
        notes: Vec<String>,
        is_result: bool,
    },
    Prose {
        content: String,
    },
    Divider,
}

impl Document {
    pub fn new(title: &str) -> Self {
        Document {
            title: title.to_string(),
            theme: Theme::default(),
            blocks: Vec::new(),
        }
    }

    pub fn step_count(&self) -> usize {
        self.blocks.iter().filter(|b| matches!(b, Block::Step { .. })).count()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Selection {
    pub step_id: usize,
    pub title: String,
    pub latex: String,
    pub unicode: String,
    pub formatted: String,
    pub notes: Vec<String>,
    pub selected_at: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_document_new_defaults() {
        let doc = Document::new("My Title");
        assert_eq!(doc.title, "My Title");
        assert_eq!(doc.theme, Theme::Dark);
        assert!(doc.blocks.is_empty());
    }

    #[test]
    fn test_document_new_untitled() {
        let doc = Document::new("Untitled");
        assert_eq!(doc.title, "Untitled");
    }

    #[test]
    fn test_step_count_empty() {
        let doc = Document::new("Test");
        assert_eq!(doc.step_count(), 0);
    }

    #[test]
    fn test_step_count_only_steps() {
        let doc = Document {
            title: "T".to_string(),
            theme: Theme::Dark,
            blocks: vec![
                Block::Step {
                    id: 1,
                    title: "A".to_string(),
                    equations: vec![],
                    notes: vec![],
                    is_result: false,
                },
                Block::Step {
                    id: 2,
                    title: "B".to_string(),
                    equations: vec![],
                    notes: vec![],
                    is_result: false,
                },
            ],
        };
        assert_eq!(doc.step_count(), 2);
    }

    #[test]
    fn test_step_count_mixed_blocks() {
        let doc = Document {
            title: "T".to_string(),
            theme: Theme::Dark,
            blocks: vec![
                Block::Prose {
                    content: "intro".to_string(),
                },
                Block::Step {
                    id: 1,
                    title: "S1".to_string(),
                    equations: vec![],
                    notes: vec![],
                    is_result: false,
                },
                Block::Divider,
                Block::Step {
                    id: 2,
                    title: "S2".to_string(),
                    equations: vec![],
                    notes: vec![],
                    is_result: true,
                },
                Block::Prose {
                    content: "outro".to_string(),
                },
            ],
        };
        assert_eq!(doc.step_count(), 2);
    }

    #[test]
    fn test_theme_default() {
        assert_eq!(Theme::default(), Theme::Dark);
    }

    #[test]
    fn test_theme_equality() {
        assert_eq!(Theme::Dark, Theme::Dark);
        assert_eq!(Theme::Light, Theme::Light);
        assert_ne!(Theme::Dark, Theme::Light);
    }
}