devjournal 0.1.0

A dev first cli journaling tool
Documentation
use crate::domain as domain_models;
use crossterm::{
    cursor::{MoveTo, position},
    execute,
    style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
    terminal::{ScrollUp, size},
};
use std::io::{Result, Stdout, Write, stdout};

fn ensure_space(stdout: &mut Stdout, y: u16, lines_needed: u16, height: u16) -> Result<u16> {
    // height is 1-based (e.g., 24 means lines 0..23)
    let available_rows = height.saturating_sub(1); // avoid writing on last line directly
    if y + lines_needed > available_rows {
        let scroll_lines = y + lines_needed - available_rows;
        execute!(stdout, ScrollUp(scroll_lines))?;
        let new_y = y.saturating_sub(scroll_lines);
        Ok(new_y)
    } else {
        Ok(y)
    }
}

fn draw_box_around_lines(stdout: &mut impl Write, x: u16, y: u16, lines: &[String]) -> Result<()> {
    // Find longest line length
    let max_len = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
    let width = max_len + 2; // padding left & right

    // Draw top border
    execute!(stdout, MoveTo(x, y), Print(""))?;
    for _ in 0..width {
        execute!(stdout, Print(""))?;
    }
    execute!(stdout, Print(""))?;

    // Draw each line with side borders
    for (i, line) in lines.iter().enumerate() {
        let line_y = y + 1 + i as u16;

        // Left border
        execute!(stdout, MoveTo(x, line_y), Print(""))?;

        // Print line with styling
        execute!(
            stdout,
            MoveTo(x + 2, line_y),
            SetForegroundColor(Color::White),
            SetAttribute(Attribute::Bold),
            Print(line),
            ResetColor,
            SetAttribute(Attribute::Reset),
        )?;

        // Right border — pad with spaces if line is shorter than max_len
        let padding = (max_len as usize).saturating_sub(line.len());
        let right_x = x + 2 + max_len;
        execute!(stdout, MoveTo(right_x, line_y), Print(""))?;

        // If padding needed, print spaces after line to not overwrite right border
        if padding > 0 {
            execute!(stdout, MoveTo(x + 2 + line.len() as u16, line_y))?;
            for _ in 0..padding {
                execute!(stdout, Print(" "))?;
            }
        }
    }

    // Draw bottom border below last line
    execute!(stdout, MoveTo(x, y + 1 + lines.len() as u16), Print(""))?;
    for _ in 0..width {
        execute!(stdout, Print(""))?;
    }
    execute!(stdout, Print(""))?;
    Ok(())
}

fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
    let mut lines = Vec::new();
    let mut current_line = String::new();

    for word in text.split_whitespace() {
        let word_len = word.len();
        let line_len = current_line.len();

        // If adding word would exceed max width, push current line and start new
        if line_len + if line_len > 0 { 1 } else { 0 } + word_len > max_width
            && !current_line.is_empty()
        {
            lines.push(current_line);
            current_line = String::new();
        }

        if !current_line.is_empty() {
            current_line.push(' ');
        }
        current_line.push_str(word);
    }

    if !current_line.is_empty() {
        lines.push(current_line);
    }

    lines
}

pub fn render_page(page: &domain_models::Page) -> Result<()> {
    let mut stdout = stdout();
    let (x, mut y) = position()?; // current cursor position
    let (term_width, term_height) = size()?; // terminal size
    let wrap_width = ((term_width as f32) * 0.7).floor() as usize;

    // Prepare header lines and wrap
    let header_line = format!(
        "📆 {} - Found {} entr{}",
        page.date.date(),
        page.entries.len(),
        if page.entries.len() == 1 { "y" } else { "ies" }
    );
    let header_lines = wrap_text(&header_line, wrap_width);

    // Calculate lines needed for header box:
    let header_box_height = header_lines.len() as u16 + 2;
    y = ensure_space(&mut stdout, y, header_box_height, term_height)?;
    draw_box_around_lines(&mut stdout, x, y, &header_lines)?;
    y += header_box_height + 1; // move cursor below box + blank line

    let mut first = true;
    for entry in &page.entries {
        if !first {
            // print a blank line before the entry
            y += 1;
        }
        first = false;

        let mut display_title = entry.title.clone();

        if entry.starred {
            display_title = format!("{}", display_title);
        }

        display_title = format!("[{}] {}", entry.date.format("%H:%M:%S"), display_title);
        let title_lines = wrap_text(&display_title, wrap_width);
        let body_lines = wrap_text(&entry.body, wrap_width);

        let total_lines_needed = title_lines.len() as u16 + body_lines.len() as u16 + 1; // +1 blank line

        y = ensure_space(&mut stdout, y, total_lines_needed, term_height)?;

        // Print title lines
        for line in &title_lines {
            execute!(
                stdout,
                MoveTo(x, y),
                SetForegroundColor(Color::Cyan),
                SetAttribute(Attribute::Bold),
                Print(line),
                ResetColor,
                SetAttribute(Attribute::Reset)
            )?;
            y += 1;
        }

        // Print body lines with prefix "| "
        for line in &body_lines {
            execute!(stdout, MoveTo(x, y), Print("| "), Print(line))?;
            y += 1;
        }

        if !entry.tags.is_empty() {
            // Spacer before tags
            execute!(stdout, MoveTo(x, y), Print("|"))?;
            y += 1;

            for tag in &entry.tags {
                let tag_line = format!("#{}", tag);
                execute!(
                    stdout,
                    MoveTo(x, y),
                    Print("| "),
                    SetForegroundColor(Color::Yellow),
                    SetAttribute(Attribute::Bold),
                    Print(tag_line),
                    ResetColor,
                    SetAttribute(Attribute::Reset)
                )?;
                y += 1;
            }

            // Final vertical bar after tags
            execute!(stdout, MoveTo(x, y), Print("|"))?;
            y += 1;
        } else {
            // Just a blank line after body if no tags
            y += 1;
        }
    }
    y = ensure_space(&mut stdout, y, 2, term_height)?;
    execute!(stdout, MoveTo(x, y),)?;
    writeln!(stdout)?;
    stdout.flush()?;
    Ok(())
}