clin-rs 0.8.12

Encrypted terminal note-taking app inspired by Obsidian
use crate::draw::input::handle_event;
use crate::draw::render::draw_canvas;
use crate::draw::state::{DrawData, Viewport};
use crate::keybinds::Keybinds;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use std::io::Stdout;

pub enum DrawEventAction {
    Quit,
    Save,
}

use ratatui::layout::Rect;

use ratatui_textarea::TextArea;
use std::time::Instant;

pub struct DrawAppState {
    pub data: DrawData,
    pub viewport: Viewport,
    pub storage: crate::storage::Storage,
    pub current_file: Option<String>,
    pub running: bool,
    pub active_tool: crate::draw::state::DrawTool,
    pub current_stroke: Option<crate::draw::state::Stroke>,
    pub last_area: Rect,
    pub last_mouse_pos: Option<(u16, u16)>,
    pub text_editor: Option<(usize, TextArea<'static>)>,
    pub last_click: Option<(u16, u16, Instant)>,
    pub theme: crate::app_theme::AppThemeColors,
    pub active_shape_type: crate::draw::state::DrawShapeType,
    pub show_shape_selector: bool,
    pub creation_origin: Option<(f64, f64)>,
    pub preview_element: Option<crate::draw::state::DrawElement>,
}

impl DrawAppState {
    pub fn new(
        storage: crate::storage::Storage,
        file_id: Option<String>,
        theme: crate::app_theme::AppThemeColors,
    ) -> Self {
        let mut data = DrawData::default();
        if let Some(id) = &file_id {
            let path = storage.note_path(id);
            if path.exists()
                && let Ok(content) = std::fs::read_to_string(path)
                && let Ok(loaded_data) = serde_json::from_str(&content)
            {
                data = loaded_data;
            }
        }

        Self {
            data,
            viewport: Viewport::default(),
            storage,
            current_file: file_id,
            running: true,
            active_tool: crate::draw::state::DrawTool::Draw,
            current_stroke: None,
            last_area: Rect::default(),
            last_mouse_pos: None,
            text_editor: None,
            last_click: None,
            theme,
            active_shape_type: crate::draw::state::DrawShapeType::Rect,
            show_shape_selector: false,
            creation_origin: None,
            preview_element: None,
        }
    }

    pub fn save_draw(&self) -> anyhow::Result<()> {
        if let Some(id) = &self.current_file {
            let path = self.storage.note_path(id);
            let content = serde_json::to_string(&self.data)?;
            std::fs::write(path, content)?;
        }
        Ok(())
    }
}

pub fn run_draw_view(
    terminal: &mut Terminal<CrosstermBackend<Stdout>>,
    storage: crate::storage::Storage,
    keybinds: &Keybinds,
    file_id: Option<String>,
    theme: crate::app_theme::AppThemeColors,
) -> anyhow::Result<Option<String>> {
    let mut app_state = DrawAppState::new(storage, file_id, theme);

    while app_state.running {
        terminal.draw(|frame| {
            let full = frame.area();
            let outer = ratatui::layout::Layout::default()
                .direction(ratatui::layout::Direction::Vertical)
                .constraints([
                    ratatui::layout::Constraint::Length(1),
                    ratatui::layout::Constraint::Min(0),
                ])
                .split(full);
            crate::ui::draw_view_title_bar(frame, outer[0], "Draw", &app_state.theme);
            app_state.last_area = outer[1];
            draw_canvas(frame, &app_state, outer[1]);
        })?;

        if crossterm::event::poll(std::time::Duration::from_millis(16))? {
            loop {
                let ev = crossterm::event::read()?;
                if let crossterm::event::Event::Resize(_, _) = ev {
                    terminal.autoresize()?;
                    let _ = terminal.clear();
                }
                if let Some(action) = handle_event(ev, &mut app_state, keybinds)? {
                    match action {
                        DrawEventAction::Quit => {
                            app_state.running = false;
                        }
                        DrawEventAction::Save => {
                            app_state.save_draw()?;
                        }
                    }
                }

                if !app_state.running
                    || !crossterm::event::poll(std::time::Duration::from_millis(0))?
                {
                    break;
                }
            }
        }
    }

    app_state.save_draw()?;
    Ok(None)
}