diff --git a/src/app.rs b/src/app.rs index f5457f9..6722892 100644 --- a/src/app.rs +++ b/src/app.rs @@ -76,6 +76,8 @@ pub struct App { pub pages: StatefulList, /// Page Info pub page_info: DnotePageInfo, + pub show_popup: bool, + pub popup_content: String, } impl Default for App { @@ -85,6 +87,8 @@ impl Default for App { match books_result { Ok(books) => Self { running: true, + show_popup: false, + popup_content: String::from(""), dnote_client: DnoteClient {}, selected_section: TuiSection::BOOKS, books: StatefulList::with_items(books), @@ -97,6 +101,8 @@ impl Default for App { println!("Something went wrong {:?}", e); Self { running: true, + show_popup: false, + popup_content: String::from(""), dnote_client: DnoteClient {}, selected_section: TuiSection::BOOKS, books: StatefulList::with_items(vec![]), diff --git a/src/dnote_lib.rs b/src/dnote_lib.rs index 670fe43..7dd83c8 100644 --- a/src/dnote_lib.rs +++ b/src/dnote_lib.rs @@ -1,5 +1,7 @@ use std::{process::Command, str::FromStr}; +type NoteId = u32; + #[derive(Debug, Clone)] pub struct DnoteBook { pub name: String, @@ -15,8 +17,8 @@ impl FromStr for DnoteBook { #[derive(Debug, Clone)] pub struct DnotePage { - pub id: u32, - /// Truncated content from page + pub id: NoteId, + /// Truncated content from the page pub summary: String, } @@ -24,7 +26,11 @@ impl FromStr for DnotePage { type Err = (); fn from_str(s: &str) -> Result { let parts: Vec<&str> = s.split(')').collect(); - let id = parts[0].trim().trim_start_matches('(').parse().unwrap(); + let id = parts[0] + .trim() + .trim_start_matches('(') + .parse() + .map_err(|_| ())?; let summary = parts[1] .trim() .trim_end_matches("[---More---]") @@ -47,6 +53,36 @@ impl FromStr for DnotePageInfo { } } +#[derive(Debug)] +pub enum DnoteCommand { + Add { + book_name: String, + note: String, + }, + ViewBooks, + ViewByBook { + book_name: String, + }, + ViewByNoteId { + note_id: NoteId, + }, + EditNoteById { + note_id: String, + new_content: Option, + new_book: Option, + }, + EditBook { + book_name: String, + new_name: Option, + }, + RemoveBook { + book_name: String, + }, + RemoveNoteById { + note_id: NoteId, + }, +} + #[derive(Debug)] pub struct DnoteClient {} @@ -59,46 +95,103 @@ pub enum DnoteClientError { } impl DnoteClient { - pub fn get_books(&self) -> Result, DnoteClientError> { - // println!("Viewing all books..."); + fn execute_command(&self, command: DnoteCommand) -> Result { + let (cmd, args) = match command { + DnoteCommand::Add { book_name, note } => { + let args = vec![book_name, "-c".to_string(), note]; + ("add", args) + } + DnoteCommand::ViewBooks => { + let args = vec!["--name-only".to_string()]; + ("view", args) + } + DnoteCommand::ViewByBook { book_name } => { + let args = vec![book_name]; + ("view", args) + } + DnoteCommand::ViewByNoteId { note_id } => { + let args = vec![note_id.to_string(), "--content-only".to_string()]; + ("view", args) + } + DnoteCommand::EditNoteById { + note_id, + new_content, + new_book, + } => { + let mut args = vec![note_id]; + if let Some(content) = new_content { + args.push("-c".to_string()); + args.push(content); + } + if let Some(book) = new_book { + args.push("-b".to_string()); + args.push(book); + } + ("edit", args) + } + DnoteCommand::EditBook { + book_name, + new_name, + } => { + let mut args = vec![book_name]; + if let Some(name) = new_name { + args.push("-n".to_string()); + args.push(name); + } + ("edit", args) + } + DnoteCommand::RemoveBook { book_name } => { + let args = vec![book_name]; + ("rm", args) + } + DnoteCommand::RemoveNoteById { note_id } => { + let args = vec![note_id.to_string()]; + ("rm", args) + } + }; let output = Command::new("dnote") - .arg("view") - .arg("--name-only") + .arg(cmd) + .args(args) .output() .map_err(|_| DnoteClientError::DnoteCommand)?; let stdout: String = String::from_utf8(output.stdout).map_err(|_| DnoteClientError::UTF8ParseError)?; - let result: Result, _> = stdout.lines().map(|l| l.parse()).collect(); + Ok(stdout) + } + + pub fn get_books(&self) -> Result, DnoteClientError> { + let output = self.execute_command(DnoteCommand::ViewBooks)?; + let result: Result, _> = output.lines().map(|l| l.parse()).collect(); result.map_err(|_| DnoteClientError::ParseError) } + + pub fn rename_book( + &self, + book_name: &str, + new_book_name: &str, + ) -> Result<(), DnoteClientError> { + self.execute_command(DnoteCommand::EditBook { + book_name: book_name.to_string(), + new_name: Some(new_book_name.to_string()), + })?; + Ok(()) + } + pub fn get_pages(&self, book_name: &str) -> Result, DnoteClientError> { - // println!("Viewing pages for book: {}", book_name); - let output = Command::new("dnote") - .arg("view") - .arg(book_name) - .output() - .map_err(|_| DnoteClientError::DnoteCommand)?; - let stdout = - String::from_utf8(output.stdout).map_err(|_| DnoteClientError::UTF8ParseError)?; - let result: Result, _> = stdout + let output = self.execute_command(DnoteCommand::ViewByBook { + book_name: book_name.to_string(), + })?; + let result: Result, _> = output .lines() - // skip first line e.g ' • on book ccu' - .skip(1) + .skip(1) // skip first line e.g ' • on book ccu' .map(|l| l.parse()) .collect(); result.map_err(|_| DnoteClientError::ParseError) } - pub fn get_page_content(&self, page_id: u32) -> Result { - // println!("Viewing content for page with id {}", page_id); - let output = Command::new("dnote") - .arg("view") - .arg(page_id.to_string()) - .arg("--content-only") - .output() - .map_err(|_| DnoteClientError::DnoteCommand)?; - let stdout = - String::from_utf8(output.stdout).map_err(|_| DnoteClientError::UTF8ParseError)?; - stdout.parse().map_err(|_| DnoteClientError::ParseError) + + pub fn get_page_content(&self, page_id: NoteId) -> Result { + let output = self.execute_command(DnoteCommand::ViewByNoteId { note_id: page_id })?; + output.parse().map_err(|_| DnoteClientError::ParseError) } } diff --git a/src/handler.rs b/src/handler.rs index 6b594c9..6a29f42 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,47 +3,87 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { - match key_event.code { - // Exit application on `ESC` or `q` - KeyCode::Esc | KeyCode::Char('q') => { - app.quit(); + if app.show_popup { + match key_event.code { + KeyCode::Char(c) => { + app.popup_content.push(c); + } + KeyCode::Backspace => { + app.popup_content.pop(); + } + KeyCode::Enter => { + if app.show_popup { + let selected_index = app.books.state.selected().unwrap_or(0); + let old_name = &app.books.items[selected_index].name; + let new_name = &app.popup_content; + if let Err(e) = app.dnote_client.rename_book(old_name, new_name) { + println!("Error renaming book: {:?}", e); + // Handle error (e.g., show an error message to the user) + } else { + // Update the book's name in the UI + app.books.items[selected_index].name.clone_from(new_name); + app.show_popup = false; + } + } + } + KeyCode::Esc => { + app.show_popup = false; + } + _ => {} } - // Exit application on `Ctrl-C` - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { + } else { + match key_event.code { + // Exit application on `ESC` or `q` + KeyCode::Esc | KeyCode::Char('q') => { app.quit(); } - } - // Counter handlers - KeyCode::Left | KeyCode::Char('h') => { - app.pages.unselect(); - app.select_prev_section(); - match app.selected_section { - TuiSection::BOOKS => {} - TuiSection::PAGES => app.books.previous(), - TuiSection::CONTENT => app.pages.previous(), + // Exit application on `Ctrl-C` + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.quit(); + } } - } - KeyCode::Right | KeyCode::Char('l') => { - match app.selected_section { - TuiSection::BOOKS => app.pages.next(), - TuiSection::PAGES => {} + // Counter handlers + KeyCode::Left | KeyCode::Char('h') => { + app.pages.unselect(); + app.select_prev_section(); + match app.selected_section { + TuiSection::BOOKS => {} + TuiSection::PAGES => app.books.previous(), + TuiSection::CONTENT => app.pages.previous(), + } + } + KeyCode::Right | KeyCode::Char('l') => { + match app.selected_section { + TuiSection::BOOKS => app.pages.next(), + TuiSection::PAGES => {} + _ => {} + } + app.select_next_section(); + } + KeyCode::Up | KeyCode::Char('k') => match app.selected_section { + TuiSection::BOOKS => app.books.previous(), + TuiSection::PAGES => app.pages.previous(), + _ => {} + }, + KeyCode::Down | KeyCode::Char('j') => match app.selected_section { + TuiSection::BOOKS => app.books.next(), + TuiSection::PAGES => app.pages.next(), _ => {} + }, + KeyCode::Char('r') => { + if app.selected_section == TuiSection::BOOKS { + app.show_popup = true; + if app.show_popup { + app.popup_content.clone_from( + &app.books.items[app.books.state.selected().unwrap_or(0)].name, + ); + } + } } - app.select_next_section(); - } - KeyCode::Up | KeyCode::Char('k') => match app.selected_section { - TuiSection::BOOKS => app.books.previous(), - TuiSection::PAGES => app.pages.previous(), - _ => {} - }, - KeyCode::Down | KeyCode::Char('j') => match app.selected_section { - TuiSection::BOOKS => app.books.next(), - TuiSection::PAGES => app.pages.next(), + // Other handlers you could add here. _ => {} - }, - // Other handlers you could add here. - _ => {} + } } Ok(()) } diff --git a/src/ui.rs b/src/ui.rs index ec9a2a7..acb56b0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,8 +1,10 @@ use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout}, + prelude::Rect, style::{Color, Modifier, Style}, - widgets::{Block, Borders, List, ListItem, Paragraph}, + text::Text, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, Frame, }; @@ -94,4 +96,40 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { .style(Style::default().fg(Color::Gray)) .block(content_block); frame.render_widget(paragraph, page_content_chunk); + + if app.show_popup { + let input = Paragraph::new(Text::from(app.popup_content.as_str())) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL).title("Rename Book")); + let area = centered_rect(60, 20, frame.size()); + frame.render_widget(Clear, area); // Clear the background + frame.render_widget(input, area); + } +} + +/// helper function to create a centered rect using up certain percentage of the available rect `r` +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ] + .as_ref(), + ) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] }