use crate::{data, error, io, ui};
use crossterm::event::KeyCode;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
enum DisplayMode {
#[default]
Display,
Rename,
Move,
Delete,
}
pub struct DisplayScreen {
manager: io::FileManager,
builder: io::HtmlBuilder,
styles: ui::UiStyles,
note: data::Note,
index: data::NoteIndexContainer,
links: [Vec<(String, String)>; 4],
name_area: tui_textarea::TextArea<'static>,
selected: [usize; 4],
foc_table: usize,
mode: DisplayMode,
}
impl DisplayScreen {
pub fn new(
note_id: &str,
index: data::NoteIndexContainer,
manager: io::FileManager,
builder: io::HtmlBuilder,
styles: ui::UiStyles,
) -> error::Result<Self> {
let index_b = index.borrow();
let note = index_b
.get(note_id)
.ok_or_else(|| error::RucolaError::NoteNotFound(note_id.to_owned()))
.cloned()?;
let l1links = index_b.links_vec(note_id);
let l2links = l1links
.iter()
.flat_map(|(id, _name)| index_b.links_vec(id))
.unique()
.collect_vec();
let l1blinks = index_b.blinks_vec(note_id);
let l2blinks = l1blinks
.iter()
.flat_map(|(id, _name)| index_b.blinks_vec(id))
.unique()
.collect();
let mut name_area = tui_textarea::TextArea::default();
name_area.set_style(styles.input_style);
name_area.set_cursor_line_style(styles.input_style);
let title_top = block::Title::from(Line::from(vec![Span::styled(
"Enter note name...",
styles.title_style,
)]));
name_area.set_block(Block::bordered().title(title_top));
drop(index_b);
Ok(Self {
links: [l1blinks, l1links, l2blinks, l2links],
note,
index,
manager,
builder,
styles,
name_area,
selected: [0; 4],
foc_table: 0,
mode: DisplayMode::Display,
})
}
fn set_name_area(&mut self, title: &str, content: Option<String>) {
let title_top = block::Title::from(Line::from(vec![Span::styled(
title.to_owned(),
self.styles.title_style,
)]));
self.name_area.set_block(Block::bordered().title(title_top));
if let Some(content) = content {
self.name_area.insert_str(content);
}
}
}
impl super::Screen for DisplayScreen {
fn draw(
&self,
area: ratatui::prelude::layout::Rect,
buf: &mut ratatui::prelude::buffer::Buffer,
) {
let vertical = Layout::vertical([
Constraint::Length(1),
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Fill(1),
]);
let [title_area, stats_area, links1_area, links2_area] = vertical.areas(area);
let title = Line::from(vec![Span::styled(
self.note.name.as_str(),
self.styles.title_style,
)])
.alignment(Alignment::Center);
let version = Line::from(vec![Span::styled(
format!("rucola v{}", env!("CARGO_PKG_VERSION")),
self.styles.subtitle_style,
)])
.alignment(Alignment::Right);
let instructions_bot_right = block::Title::from(Line::from(vec![
Span::styled("V", self.styles.hotkey_style),
Span::styled("iew──", self.styles.text_style),
Span::styled("E", self.styles.hotkey_style),
Span::styled("dit──", self.styles.text_style),
Span::styled("R", self.styles.hotkey_style),
Span::styled("ename──", self.styles.text_style),
Span::styled("M", self.styles.hotkey_style),
Span::styled("ove──", self.styles.text_style),
Span::styled("D", self.styles.hotkey_style),
Span::styled("elete", self.styles.text_style),
]))
.alignment(Alignment::Right)
.position(block::Position::Bottom);
let stats = self.note.to_stats_table(&self.styles).block(
Block::bordered()
.title(style::Styled::set_style(
"Statistics",
self.styles.title_style,
))
.title(instructions_bot_right),
);
let horizontal = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]);
let [blinks1, links1] = horizontal.areas(links1_area);
let [blinks2, links2] = horizontal.areas(links2_area);
Widget::render(title, title_area, buf);
Widget::render(version, title_area, buf);
Widget::render(stats, stats_area, buf);
self.draw_link_table(0, "Backlinks", blinks1, buf);
self.draw_link_table(1, "Links", links1, buf);
self.draw_link_table(2, "Level 2 Backlinks", blinks2, buf);
self.draw_link_table(3, "Level 2 Links", links2, buf);
if self.mode == DisplayMode::Rename
|| self.mode == DisplayMode::Move
|| self.mode == DisplayMode::Delete
{
let popup_areas = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
])
.split(area);
let center_area = Layout::horizontal([
Constraint::Fill(1),
Constraint::Percentage(60),
Constraint::Fill(1),
])
.split(popup_areas[1])[1];
Widget::render(Clear, center_area, buf);
if self.mode == DisplayMode::Delete {
let keys = block::Title::from(Line::from(vec![
Span::styled("", self.styles.hotkey_style),
Span::styled(": Delete─", self.styles.text_style),
Span::styled("Other", self.styles.hotkey_style),
Span::styled(": Abort", self.styles.text_style),
]))
.alignment(Alignment::Center)
.position(block::Position::Bottom);
let del = Paragraph::new(Span::styled(
"Are you sure you want to delete?\n",
self.styles.text_style,
))
.alignment(Alignment::Center)
.block(Block::bordered().title(keys));
Widget::render(del, center_area, buf);
} else {
let name_input = self.name_area.widget();
Widget::render(name_input, center_area, buf);
}
}
}
fn update(&mut self, key: crossterm::event::KeyEvent) -> error::Result<ui::Message> {
match self.mode {
DisplayMode::Display => match key.code {
KeyCode::Char('Q' | 'q') => {
return Ok(ui::Message::Quit);
}
KeyCode::Char('F' | 'f') => {
return Ok(ui::Message::DisplayStackClear);
}
KeyCode::Left | KeyCode::Char('H' | 'h') => {
return Ok(ui::Message::DisplayStackPop);
}
KeyCode::Up | KeyCode::Char('K' | 'k') => {
if let Some(selected) = self.selected.get_mut(self.foc_table) {
*selected = selected.saturating_sub(1);
}
}
KeyCode::Down | KeyCode::Char('J' | 'j') => {
if let Some(selected) = self.selected.get_mut(self.foc_table) {
*selected = selected.saturating_add(1).min(
self.links
.get(self.foc_table)
.map(|list| list.len().saturating_sub(1))
.unwrap_or_default(),
);
}
}
KeyCode::Tab => {
self.foc_table = (self.foc_table.wrapping_add(1)) % 4;
}
KeyCode::BackTab => {
self.foc_table = (self.foc_table.wrapping_sub(1)) % 4;
}
KeyCode::Enter | KeyCode::Right | KeyCode::Char('L' | 'l') => {
return Ok(self
.links
.get(self.foc_table)
.and_then(|table| table.get(self.selected[self.foc_table]))
.map(|(id, _name)| ui::Message::DisplayStackPush(id.to_owned()))
.unwrap_or(ui::Message::None));
}
KeyCode::Char('e' | 'E') => {
return Ok(ui::Message::OpenExternalCommand(
self.manager.create_edit_command(&self.note.path)?,
));
}
KeyCode::Char('v' | 'V') => {
self.builder.create_html(&self.note, true)?;
return Ok(ui::Message::OpenExternalCommand(
self.builder.create_view_command(&self.note)?,
));
}
KeyCode::Char('r' | 'R') => {
self.mode = DisplayMode::Rename;
self.set_name_area("Enter new name of note...", Some(self.note.name.clone()));
}
KeyCode::Char('m' | 'M') => {
self.mode = DisplayMode::Move;
self.set_name_area("Enter new location relative to vault...", None);
}
KeyCode::Char('d' | 'D') => {
self.mode = DisplayMode::Delete;
}
_ => {}
},
DisplayMode::Rename => match key.code {
KeyCode::Esc => {
super::extract_string_and_clear(&mut self.name_area);
self.mode = DisplayMode::Display;
}
KeyCode::Enter => {
self.mode = DisplayMode::Display;
self.manager.rename_note_file(
self.index.clone(),
&data::name_to_id(&self.note.name),
super::extract_string_and_clear(&mut self.name_area).ok_or_else(|| {
error::RucolaError::Input("New name is empty.".to_string())
})?,
)?;
}
_ => {
self.name_area.input(key);
}
},
DisplayMode::Move => match key.code {
KeyCode::Esc => {
super::extract_string_and_clear(&mut self.name_area);
self.mode = DisplayMode::Display;
}
KeyCode::Enter => {
self.mode = DisplayMode::Display;
self.manager.move_note_file(
self.index.clone(),
&data::name_to_id(&self.note.name),
super::extract_string_and_clear(&mut self.name_area).ok_or_else(|| {
error::RucolaError::Input("Move location is empty.".to_string())
})?,
)?;
}
_ => {
self.name_area.input(key);
}
},
DisplayMode::Delete => match key.code {
KeyCode::Enter => {
self.manager
.delete_note_file(self.index.clone(), &data::name_to_id(&self.note.name))?;
return Ok(ui::Message::DisplayStackPop);
}
_ => {
self.mode = DisplayMode::Display;
}
},
}
Ok(ui::Message::None)
}
}
impl DisplayScreen {
fn draw_link_table(&self, index: usize, title: &str, area: Rect, buf: &mut Buffer) {
let title = block::Title::from(Line::from(vec![Span::styled(
title,
self.styles.title_style,
)]))
.alignment(Alignment::Left)
.position(block::Position::Top);
let count = self
.links
.get(index)
.map(|list| list.len())
.unwrap_or_default();
let count = block::Title::from(Line::from(vec![Span::styled(
format!("{} Note{}", count, if count == 1 { "" } else { "s" }),
self.styles.text_style,
)]))
.alignment(Alignment::Right)
.position(block::Position::Top);
let mut state = self
.selected
.get(index)
.map(|selected_index| {
TableState::new()
.with_offset(
selected_index.saturating_sub(area.height as usize / 3).min(
self.links
.get(index)
.map(|list| list.len())
.unwrap_or(0)
.saturating_add(2)
.saturating_sub(area.height as usize),
),
)
.with_selected(Some(*selected_index))
})
.unwrap_or_default();
*state.offset_mut() = self.selected[index]
.saturating_sub(area.height as usize / 3)
.min(
self.links
.get(index)
.map(|list| list.len())
.unwrap_or(0)
.saturating_sub(area.height as usize)
.saturating_add(2),
);
let rows = self
.links
.get(index)
.map(|list| {
list.iter()
.map(|(_id, name)| {
Row::new(vec![Span::from(name).style(self.styles.text_style)])
})
.collect_vec()
})
.unwrap_or_default();
let block = Block::bordered().title(title).title(count);
let block = match index {
2 => block.title(
block::Title::from(Line::from(vec![
Span::styled("J", self.styles.hotkey_style),
Span::styled("/", self.styles.text_style),
Span::styled("", self.styles.hotkey_style),
Span::styled(": Down──", self.styles.text_style),
Span::styled("K", self.styles.hotkey_style),
Span::styled("/", self.styles.text_style),
Span::styled("", self.styles.hotkey_style),
Span::styled(": Up──", self.styles.text_style),
Span::styled("L", self.styles.hotkey_style),
Span::styled("/", self.styles.text_style),
Span::styled("", self.styles.hotkey_style),
Span::styled("/", self.styles.text_style),
Span::styled("", self.styles.hotkey_style),
Span::styled(": Open──", self.styles.text_style),
Span::styled("H", self.styles.hotkey_style),
Span::styled("/", self.styles.text_style),
Span::styled("", self.styles.hotkey_style),
Span::styled(": Back──", self.styles.text_style),
Span::styled("F", self.styles.hotkey_style),
Span::styled(": Home", self.styles.text_style),
]))
.alignment(Alignment::Left)
.position(block::Position::Bottom),
),
3 => block.title(
block::Title::from(Line::from(vec![
Span::styled("Tab", self.styles.hotkey_style),
Span::styled(": Next Table──", self.styles.text_style),
Span::styled("Shift+Tab", self.styles.hotkey_style),
Span::styled(": Previous Table", self.styles.text_style),
]))
.alignment(Alignment::Right)
.position(block::Position::Bottom),
),
_ => block,
};
let table = Table::new(rows, [Constraint::Min(20)])
.highlight_style(if index == self.foc_table {
self.styles.selected_style
} else {
self.styles.text_style
})
.block(block);
StatefulWidget::render(table, area, buf, &mut state);
}
}