tuisky 0.2.2

TUI client for Bluesky
Documentation
use super::super::views::types::Action as ViewsAction;
use super::types::{Action, Data, ImageData};
use super::ModalComponent;
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use image::ImageReader;
use ratatui::layout::{Constraint, Layout, Margin, Rect};
use ratatui::style::{Color, Style, Stylize};
use ratatui::text::Line;
use ratatui::widgets::{Block, Clear};
use ratatui::Frame;
use std::path::PathBuf;
use tui_textarea::TextArea;

pub struct Image {
    pub path: TextArea<'static>,
    pub alt: TextArea<'static>,
}

enum Focus {
    Path,
    Alt,
    Ok,
    Delete,
}

impl Focus {
    fn next(&self, delete: bool) -> Self {
        match self {
            Self::Path => Self::Alt,
            Self::Alt => Self::Ok,
            Self::Ok if delete => Self::Delete,
            Self::Ok => Self::Ok,
            Self::Delete => Self::Delete,
        }
    }
    fn prev(&self, _: bool) -> Self {
        match self {
            Self::Path => Self::Path,
            Self::Alt => Self::Path,
            Self::Ok => Self::Alt,
            Self::Delete => Self::Ok,
        }
    }
}

enum State {
    None,
    Ok,
    Error,
}

pub struct EmbedImagesModalComponent {
    image: Image,
    focus: Focus,
    state: State,
    index: Option<usize>,
}

impl EmbedImagesModalComponent {
    pub fn new(init: Option<(usize, ImageData)>) -> Self {
        let (mut path, mut alt) = if let Some((_, init)) = &init {
            (
                TextArea::new(vec![init.path.clone()]),
                TextArea::new(init.alt.lines().map(String::from).collect()),
            )
        } else {
            (TextArea::default(), TextArea::default())
        };
        path.set_block(Block::bordered().title("Path"));
        path.set_cursor_line_style(Style::default());
        alt.set_block(Block::bordered().title("Alt").dim());
        alt.set_cursor_line_style(Style::default());
        alt.set_cursor_style(Style::default());
        let image = Image { path, alt };

        let mut ret = Self {
            image,
            focus: Focus::Path,
            state: State::None,
            index: init.map(|(i, _)| i),
        };
        ret.check_path();
        ret
    }
    fn check_path(&mut self) {
        if let Some(block) = self.image.path.block() {
            let block = block.clone();
            let path = PathBuf::from(self.image.path.lines().join(""));
            self.state = if let Ok(metadata) = path.metadata() {
                if metadata.is_file()
                    && metadata.len() <= 1_000_000
                    && ImageReader::open(path)
                        .ok()
                        .and_then(|reader| reader.decode().ok())
                        .is_some()
                {
                    State::Ok
                } else {
                    State::Error
                }
            } else {
                State::None
            };
            self.image.path.set_block(match self.state {
                State::None => block.border_style(Color::Reset),
                State::Ok => block.border_style(Color::Green),
                State::Error => block.border_style(Color::Red),
            });
        }
    }
    fn current_textarea(&mut self) -> Option<&mut TextArea<'static>> {
        match self.focus {
            Focus::Path => Some(&mut self.image.path),
            Focus::Alt => Some(&mut self.image.alt),
            _ => None,
        }
    }
    fn update_focus(&mut self, focus: Focus) {
        if let Some(curr) = self.current_textarea() {
            curr.set_cursor_style(Style::default());
            if let Some(block) = curr.block() {
                curr.set_block(block.clone().dim());
            }
        }
        self.focus = focus;
        if let Some(curr) = self.current_textarea() {
            curr.set_cursor_style(Style::default().reversed());
            if let Some(block) = curr.block() {
                curr.set_block(block.clone().reset());
            }
        }
    }
}

impl ModalComponent for EmbedImagesModalComponent {
    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
        match self.focus {
            Focus::Path => {
                if matches!(
                    (key.code, key.modifiers),
                    (KeyCode::Enter, _) | (KeyCode::Char('m'), KeyModifiers::CONTROL)
                ) {
                    return Ok(None);
                }
                let cursor = self.image.path.cursor();
                return Ok(if self.image.path.input(key) {
                    self.check_path();
                    Some(Action::Render)
                } else if self.image.path.cursor() != cursor {
                    Some(Action::Render)
                } else {
                    None
                });
            }
            Focus::Alt => {
                let cursor = self.image.alt.cursor();
                return Ok(
                    if self.image.alt.input(key) || self.image.alt.cursor() != cursor {
                        Some(Action::Render)
                    } else {
                        None
                    },
                );
            }
            _ => {}
        }
        Ok(None)
    }
    fn update(&mut self, action: ViewsAction) -> Result<Option<Action>> {
        Ok(match action {
            ViewsAction::NextItem => {
                self.update_focus(self.focus.next(self.index.is_some()));
                Some(Action::Render)
            }
            ViewsAction::PrevItem => {
                self.update_focus(self.focus.prev(self.index.is_some()));
                Some(Action::Render)
            }
            ViewsAction::Enter => match self.focus {
                Focus::Ok => {
                    if let State::Ok = self.state {
                        Some(Action::Ok(Box::new(Data::Image((
                            ImageData {
                                path: self.image.path.lines().join(""),
                                alt: self.image.alt.lines().join("\n"),
                            },
                            self.index,
                        )))))
                    } else {
                        None
                    }
                }
                Focus::Delete => Some(Action::Delete(self.index)),
                _ => self.update(ViewsAction::NextItem)?,
            },
            ViewsAction::Back => Some(Action::Cancel),
            _ => None,
        })
    }
    fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
        let area = area.inner(Margin {
            horizontal: 2,
            vertical: 1,
        });
        let [area] = Layout::vertical([Constraint::Max(11)]).areas(area);

        let block = Block::bordered().title("Embed image");
        let inner = block.inner(area);
        f.render_widget(Clear, area);
        f.render_widget(block, area);

        let mut constraints = vec![
            Constraint::Length(3),
            Constraint::Length(4),
            Constraint::Length(1),
        ];
        if self.index.is_some() {
            constraints.push(Constraint::Length(1));
        }
        let layout = Layout::vertical(constraints).split(inner);
        let mut line = Line::from("OK").centered();
        line = match self.state {
            State::Ok => line.blue(),
            _ => line.dim(),
        };
        if let Focus::Ok = self.focus {
            line = line.reversed();
        }
        f.render_widget(&self.image.path, layout[0]);
        f.render_widget(&self.image.alt, layout[1]);
        f.render_widget(line, layout[2]);
        if let Some(area) = layout.get(3) {
            f.render_widget(
                Line::from("Delete")
                    .centered()
                    .red()
                    .patch_style(match self.focus {
                        Focus::Delete => Style::default().reversed(),
                        _ => Style::default(),
                    }),
                *area,
            )
        }
        Ok(())
    }
}