use std::sync::Arc;
use std::sync::mpsc::Receiver;
use crate::settings::themes::Theme;
use chrono::NaiveDate;
use kimun_core::SearchResult;
use kimun_core::nfs::VaultPath;
use kimun_core::{NoteVault, ResultType};
use ratatui::Frame;
use ratatui::crossterm::event::KeyCode;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::components::Component;
use crate::components::event_state::EventState;
use crate::components::events::{AppEvent, AppTx, InputEvent};
use crate::components::file_list::{FileListComponent, FileListEntry, SortField, SortOrder};
use crate::keys::KeyBindings;
use crate::settings::icons::Icons;
use crate::settings::AppSettings;
pub struct SidebarComponent {
current_dir: VaultPath,
pub file_list: FileListComponent,
pending_rx: Option<Receiver<SearchResult>>,
vault: Arc<NoteVault>,
default_sort_field: SortField,
default_sort_order: SortOrder,
journal_sort_field: SortField,
journal_sort_order: SortOrder,
}
impl SidebarComponent {
pub fn new(key_bindings: KeyBindings, vault: Arc<NoteVault>, icons: Icons, settings: &AppSettings) -> Self {
Self {
current_dir: VaultPath::root(),
file_list: FileListComponent::new(key_bindings, icons),
pending_rx: None,
vault,
default_sort_field: SortField::from(settings.default_sort_field),
default_sort_order: SortOrder::from(settings.default_sort_order),
journal_sort_field: SortField::from(settings.journal_sort_field),
journal_sort_order: SortOrder::from(settings.journal_sort_order),
}
}
pub fn current_dir(&self) -> &VaultPath {
&self.current_dir
}
pub fn is_empty(&self) -> bool {
self.file_list.is_empty()
}
pub fn start_loading(&mut self, rx: Receiver<SearchResult>, current_dir: VaultPath) {
self.current_dir = current_dir.clone();
self.file_list.clear();
self.file_list.loading = true;
if ¤t_dir == self.vault.journal_path() {
self.file_list.sort_field = self.journal_sort_field;
self.file_list.sort_order = self.journal_sort_order;
} else {
self.file_list.sort_field = self.default_sort_field;
self.file_list.sort_order = self.default_sort_order;
}
if !current_dir.is_root_or_empty() {
let parent = current_dir.get_parent_path().0;
self.file_list.add_up_entry(parent);
}
self.pending_rx = Some(rx);
self.sync_create_entry();
}
fn sync_create_entry(&mut self) {
if self.file_list.search_query.is_empty() {
self.file_list.set_create_entry(None);
} else {
let path = self
.current_dir
.append(&VaultPath::note_path_from(&self.file_list.search_query))
.flatten();
let filename = path.to_string();
self.file_list
.set_create_entry(Some(FileListEntry::CreateNote { filename, path }));
}
}
fn poll_loading(&mut self) {
let Some(rx) = &self.pending_rx else { return };
loop {
match rx.try_recv() {
Ok(result) => {
if matches!(&result.rtype, ResultType::Directory)
&& result.path == self.current_dir
{
continue;
}
let journal_date = self
.vault
.journal_date(&result.path)
.map(format_journal_date);
self.file_list
.push_entry(FileListEntry::from_result(result, journal_date));
}
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.pending_rx = None;
self.file_list.loading = false;
self.file_list.finalize_sort();
break;
}
}
}
}
}
fn format_journal_date(date: NaiveDate) -> String {
date.format("%A, %B %-d, %Y").to_string()
}
impl Component for SidebarComponent {
fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
if let InputEvent::Key(key) = event {
if key.code == KeyCode::Enter {
if let Some(FileListEntry::CreateNote { path, .. }) =
self.file_list.selected_entry()
{
let path = path.clone();
let vault = Arc::clone(&self.vault);
let tx2 = tx.clone();
tokio::spawn(async move {
if let Err(e) = vault.load_or_create_note(&path, None).await {
log::warn!("create note failed for {path}: {e}");
return;
}
tx2.send(AppEvent::OpenPath(path)).ok();
});
return EventState::Consumed;
}
}
}
let result = self.file_list.handle_input(event, tx);
if let InputEvent::Key(key) = event {
if matches!(key.code, KeyCode::Char(_) | KeyCode::Backspace) {
self.sync_create_entry();
}
}
result
}
fn hint_shortcuts(&self) -> Vec<(String, String)> {
self.file_list.hint_shortcuts()
}
fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
self.poll_loading();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(0),
])
.split(rect);
let border_style = theme.border_style(focused);
let header = Block::default()
.title(self.current_dir.to_string())
.borders(Borders::ALL)
.border_style(border_style)
.style(theme.panel_style());
let header_inner = header.inner(rows[0]);
f.render_widget(header, rows[0]);
f.render_widget(
Paragraph::new(format!("{} notes", self.file_list.note_count()))
.style(Style::default()
.fg(theme.fg_muted.to_ratatui())
.bg(theme.bg_panel.to_ratatui())),
header_inner,
);
let search_block = Block::default()
.title(" Search")
.borders(Borders::ALL)
.border_style(border_style)
.style(theme.panel_style());
let search_inner = search_block.inner(rows[1]);
f.render_widget(search_block, rows[1]);
f.render_widget(
Paragraph::new(self.file_list.search_query.as_str()).style(
Style::default()
.fg(theme.fg.to_ratatui())
.bg(theme.bg_panel.to_ratatui()),
),
search_inner,
);
if focused {
let cursor_x = search_inner.x + self.file_list.search_query.chars().count() as u16;
f.set_cursor_position((cursor_x, search_inner.y));
}
self.file_list.render(f, rows[2], theme, focused);
}
}