rura 1.3.0

Interactive TUI pipeline editor built for rapid iteration
use crate::completable_input::CompletableInput;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint::Length;
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
use ratatui::prelude::{Line, Stylize, Widget};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;

pub struct SaveToFileWidget {
    pub title: String,
    pub file_path_input: CompletableInput,
    pub error_message: Option<String>,
    pub cursor: (u16, u16),
}

impl SaveToFileWidget {
    pub fn new(title: String, shell: String) -> Self {
        Self {
            title,
            file_path_input: CompletableInput::file_only("", &shell),
            error_message: None,
            cursor: (0, 0),
        }
    }

    pub fn save(&mut self, content: &str) -> anyhow::Result<()> {
        self.save_file(content, 0o644)
    }

    pub fn save_executable(&mut self, content: &str) -> anyhow::Result<()> {
        self.save_file(content, 0o755)
    }

    fn save_file(&mut self, content: &str, mode: u32) -> anyhow::Result<()> {
        let path = PathBuf::from(self.file_path_input.value().trim());
        let mut file = OpenOptions::new()
            .create_new(true)
            .write(true)
            .mode(mode)
            .open(path)?;

        write!(file, "{}\n", content)?;

        self.error_message = None;

        Ok(())
    }
}

impl Widget for &mut SaveToFileWidget {
    fn render(self, area: Rect, buf: &mut Buffer)
    where
        Self: Sized,
    {
        let height = if self.error_message.is_some() { 7 } else { 6 };

        let centered_area = area.centered(Constraint::Percentage(60), Constraint::Length(height));

        let centered_inner_area = centered_area.inner(Margin::new(1, 1));

        let [path_area, error_area, buttons_area] = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Length(3),
                Length(if self.error_message.is_some() { 1 } else { 0 }),
                Length(1),
            ])
            .areas(centered_inner_area);

        Clear.render(centered_area, buf);
        Block::default()
            .borders(Borders::ALL)
            .title(self.title.clone())
            .white()
            .on_blue()
            .render(centered_area, buf);

        let path_input_area = centered_inner_area.inner(Margin::new(1, 1));
        let shift = self
            .file_path_input
            .cursor()
            .saturating_sub(path_input_area.width.into()) as u16;
        Paragraph::new(self.file_path_input.value())
            .block(Block::default().borders(Borders::ALL))
            .scroll((0, shift))
            .render(path_area, buf);

        if let Some(error_message) = &self.error_message {
            Line::from(error_message.clone())
                .red()
                .on_white()
                .render(error_area, buf);
        }

        Line::from(vec![
            "Enter ".bold(),
            "Save | ".into(),
            "Esc ".bold(),
            "Cancel".into(),
        ])
        .right_aligned()
        .render(buttons_area, buf);

        self.cursor = (
            (path_input_area.x + self.file_path_input.cursor() as u16)
                .min(path_input_area.width + path_area.x),
            path_input_area.y,
        );
    }
}