From 66402a9c23e0975a8a3d8c2707b689b9cde98ccf Mon Sep 17 00:00:00 2001 From: lukeflo Date: Tue, 22 Oct 2024 21:52:36 +0200 Subject: rearrange code, file and folder structure --- src/frontend/app.rs | 259 ------------------- src/frontend/entries.rs | 499 ------------------------------------ src/frontend/handler.rs | 210 --------------- src/frontend/keywords.rs | 159 ------------ src/frontend/tui.rs | 223 ---------------- src/frontend/ui.rs | 646 ----------------------------------------------- 6 files changed, 1996 deletions(-) delete mode 100644 src/frontend/app.rs delete mode 100644 src/frontend/entries.rs delete mode 100644 src/frontend/handler.rs delete mode 100644 src/frontend/keywords.rs delete mode 100644 src/frontend/tui.rs delete mode 100644 src/frontend/ui.rs (limited to 'src/frontend') diff --git a/src/frontend/app.rs b/src/frontend/app.rs deleted file mode 100644 index 822c6f0..0000000 --- a/src/frontend/app.rs +++ /dev/null @@ -1,259 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use super::tui; -use crate::backend::cliargs::CLIArgs; -use crate::backend::{bib::*, search::BibiSearch}; -use crate::{ - frontend::entries::EntryTable, frontend::handler::handle_key_events, - frontend::keywords::TagList, frontend::tui::Event, -}; -use arboard::Clipboard; -use color_eyre::eyre::{Ok, Result}; -use std::path::PathBuf; - -// Areas in which actions are possible -#[derive(Debug)] -pub enum CurrentArea { - EntryArea, - TagArea, - SearchArea, - HelpArea, - InfoArea, -} - -// Check which area was active when popup set active -#[derive(Debug)] -pub enum FormerArea { - EntryArea, - TagArea, - SearchArea, -} - -// Application. -#[derive(Debug)] -pub struct App { - // Is the application running? - pub running: bool, - // main bib file - pub main_bibfile: PathBuf, - // main bibliography - pub main_biblio: BibiMain, - // search struct: - pub search_struct: BibiSearch, - // tag list - pub tag_list: TagList, - // table items - pub entry_table: EntryTable, - // scroll state info buffer - pub scroll_info: u16, - // area - pub current_area: CurrentArea, - // mode for popup window - pub former_area: Option, -} - -impl App { - // Constructs a new instance of [`App`]. - pub fn new(args: CLIArgs) -> Result { - // Self::default() - let running = true; - let main_bibfile = args.bibfilearg; - let main_biblio = BibiMain::new(main_bibfile.clone()); - let tag_list = TagList::new(main_biblio.keyword_list.clone()); - let search_struct = BibiSearch::default(); - let entry_table = EntryTable::new(main_biblio.entry_list.clone()); - let current_area = CurrentArea::EntryArea; - Ok(Self { - running, - main_bibfile, - main_biblio, - tag_list, - search_struct, - entry_table, - scroll_info: 0, - current_area, - former_area: None, - }) - } - - pub async fn run(&mut self) -> Result<()> { - let mut tui = tui::Tui::new()?; - tui.enter()?; - - // Start the main loop. - while self.running { - // Render the user interface. - tui.draw(self)?; - // Handle events. - match tui.next().await? { - Event::Tick => self.tick(), - Event::Key(key_event) => handle_key_events(key_event, self, &mut tui)?, - Event::Mouse(_) => {} - Event::Resize(_, _) => {} - } - } - - // Exit the user interface. - tui.exit()?; - Ok(()) - } - - // Handles the tick event of the terminal. - pub fn tick(&self) {} - - // General commands - - // Set running to false to quit the application. - pub fn quit(&mut self) { - self.running = false; - } - - pub fn update_lists(&mut self) { - self.main_biblio = BibiMain::new(self.main_bibfile.clone()); - // self.tag_list = TagList::from_iter(self.main_biblio.keyword_list.clone()); - self.tag_list = TagList::new(self.main_biblio.keyword_list.clone()); - self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone()); - } - - // Toggle moveable list between entries and tags - pub fn toggle_area(&mut self) { - if let CurrentArea::EntryArea = self.current_area { - self.entry_table.entry_scroll_state = self.entry_table.entry_scroll_state.position(0); - self.current_area = CurrentArea::TagArea; - self.tag_list.tag_list_state.select(Some(0)); - self.tag_list.tag_scroll_state = self - .tag_list - .tag_scroll_state - .position(self.tag_list.tag_list_state.selected().unwrap()); - } else if let CurrentArea::TagArea = self.current_area { - self.current_area = CurrentArea::EntryArea; - self.tag_list.tag_list_state.select(None); - self.entry_table.entry_scroll_state = self - .entry_table - .entry_scroll_state - .position(self.entry_table.entry_table_state.selected().unwrap()); - } - } - - pub fn reset_current_list(&mut self) { - self.entry_table = EntryTable::new(self.main_biblio.entry_list.clone()); - self.tag_list = TagList::new(self.main_biblio.keyword_list.clone()); - if let CurrentArea::TagArea = self.current_area { - self.tag_list.tag_list_state.select(Some(0)) - } - self.entry_table.entry_table_at_search_start.clear(); - self.search_struct.filtered_tag_list.clear(); - self.search_struct.inner_search = false; - self.former_area = None - } - - // Yank the passed string to system clipboard - pub fn yank_text(selection: &str) { - let mut clipboard = Clipboard::new().unwrap(); - let yanked_text = selection.to_string(); - clipboard.set_text(yanked_text).unwrap(); - } - - pub fn scroll_info_down(&mut self) { - self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll.saturating_add(1); - self.entry_table.entry_info_scroll_state = self - .entry_table - .entry_info_scroll_state - .position(self.entry_table.entry_info_scroll.into()); - } - - pub fn scroll_info_up(&mut self) { - self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll.saturating_sub(1); - self.entry_table.entry_info_scroll_state = self - .entry_table - .entry_info_scroll_state - .position(self.entry_table.entry_info_scroll.into()); - } - - // Search Area - - // Enter the search area - pub fn enter_search_area(&mut self) { - if let CurrentArea::EntryArea = self.current_area { - if let Some(FormerArea::TagArea) = self.former_area { - self.search_struct.inner_search = true - } - self.entry_table.entry_table_at_search_start = - self.entry_table.entry_table_items.clone(); - self.former_area = Some(FormerArea::EntryArea) - } else if let CurrentArea::TagArea = self.current_area { - self.former_area = Some(FormerArea::TagArea) - } - self.current_area = CurrentArea::SearchArea - } - - // Confirm search: Search former list by pattern - pub fn confirm_search(&mut self) { - if let Some(FormerArea::EntryArea) = self.former_area { - self.current_area = CurrentArea::EntryArea; - self.entry_table.entry_table_state.select(Some(0)) - } else if let Some(FormerArea::TagArea) = self.former_area { - self.current_area = CurrentArea::TagArea; - self.tag_list.tag_list_state.select(Some(0)) - } - self.former_area = Some(FormerArea::SearchArea); - self.search_struct.search_string.clear(); - self.entry_table.entry_table_at_search_start.clear(); - } - - // Break search: leave search area without filtering list - pub fn break_search(&mut self) { - if let Some(FormerArea::EntryArea) = self.former_area { - self.current_area = CurrentArea::EntryArea; - self.entry_table.entry_table_state.select(Some(0)) - } else if let Some(FormerArea::TagArea) = self.former_area { - self.current_area = CurrentArea::TagArea; - self.tag_list.tag_list_state.select(Some(0)) - } - // But keep filtering by tag if applied before entering search area - if !self.search_struct.inner_search { - self.reset_current_list(); - } - self.former_area = None; - // If search is canceled, reset default status of struct - self.search_struct.search_string.clear(); - self.entry_table.entry_table_at_search_start.clear(); - } - - // Remove last char from search pattern and filter list immidiately - pub fn search_pattern_pop(&mut self) { - self.search_struct.search_string.pop(); - if let Some(FormerArea::EntryArea) = self.former_area { - self.search_entries(); - self.filter_tags_by_entries(); - } else if let Some(FormerArea::TagArea) = self.former_area { - self.search_tags(); - } - } - - // Add current char to search pattern and filter list immidiatley - pub fn search_pattern_push(&mut self, search_pattern: char) { - self.search_struct.search_string.push(search_pattern); - if let Some(FormerArea::EntryArea) = self.former_area { - self.search_entries(); - self.filter_tags_by_entries(); - } else if let Some(FormerArea::TagArea) = self.former_area { - self.search_tags(); - } - } -} diff --git a/src/frontend/entries.rs b/src/frontend/entries.rs deleted file mode 100644 index 7883a17..0000000 --- a/src/frontend/entries.rs +++ /dev/null @@ -1,499 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use super::app::App; -use super::tui::Tui; -use crate::backend::{bib::BibiData, search::BibiSearch}; -use color_eyre::eyre::{Context, Ok, Result}; -use core::panic; -use editor_command::EditorBuilder; -use ratatui::widgets::{ScrollbarState, TableState}; -use std::process::{Command, Stdio}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EntryTableColumn { - Authors, - Title, - Year, - Pubtype, -} - -// Define list containing entries as table -#[derive(Debug, PartialEq, Eq)] -pub struct EntryTable { - pub entry_table_items: Vec, - pub entry_table_at_search_start: Vec, - pub entry_table_selected_column: EntryTableColumn, - pub entry_table_sorted_by_col: EntryTableColumn, - pub entry_table_reversed_sort: bool, - pub entry_table_state: TableState, - pub entry_scroll_state: ScrollbarState, - pub entry_info_scroll: u16, - pub entry_info_scroll_state: ScrollbarState, -} - -impl EntryTable { - pub fn new(entry_list: Vec) -> Self { - let entry_table_items = Self::set_entry_table(entry_list); - let entry_table_state = TableState::default().with_selected(0); - let entry_scroll_state = ScrollbarState::new(entry_table_items.len()); - let entry_info_scroll_state = ScrollbarState::default(); - Self { - entry_table_items, - entry_table_at_search_start: Vec::new(), - entry_table_selected_column: EntryTableColumn::Authors, - entry_table_sorted_by_col: EntryTableColumn::Authors, - entry_table_reversed_sort: false, - entry_table_state, - entry_scroll_state, - entry_info_scroll: 0, - entry_info_scroll_state, - } - } - - pub fn set_entry_table(entry_list: Vec) -> Vec { - let mut entry_table: Vec = entry_list - .into_iter() - .map(|e| EntryTableItem { - authors: e.authors, - short_author: String::new(), - title: e.title, - year: e.year, - pubtype: e.pubtype, - keywords: e.keywords, - citekey: e.citekey, - abstract_text: e.abstract_text, - doi_url: e.doi_url, - filepath: e.filepath, - }) - .collect(); - - entry_table.sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())); - entry_table - } - - // Sort entry table by specific column. - // Toggle sorting by hitting same key again - pub fn sort_entry_table(&mut self, toggle: bool) { - if toggle { - self.entry_table_reversed_sort = !self.entry_table_reversed_sort; - } - if self.entry_table_selected_column != self.entry_table_sorted_by_col { - self.entry_table_reversed_sort = false - } - self.entry_table_sorted_by_col = self.entry_table_selected_column.clone(); - if self.entry_table_reversed_sort { - match self.entry_table_selected_column { - EntryTableColumn::Authors => self - .entry_table_items - .sort_by(|a, b| b.authors.to_lowercase().cmp(&a.authors.to_lowercase())), - EntryTableColumn::Title => self - .entry_table_items - .sort_by(|a, b| b.title.to_lowercase().cmp(&a.title.to_lowercase())), - EntryTableColumn::Year => self - .entry_table_items - .sort_by(|a, b| b.year.to_lowercase().cmp(&a.year.to_lowercase())), - EntryTableColumn::Pubtype => self - .entry_table_items - .sort_by(|a, b| b.pubtype.to_lowercase().cmp(&a.pubtype.to_lowercase())), - } - } else if !self.entry_table_reversed_sort { - match self.entry_table_selected_column { - EntryTableColumn::Authors => self - .entry_table_items - .sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())), - EntryTableColumn::Title => self - .entry_table_items - .sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), - EntryTableColumn::Year => self - .entry_table_items - .sort_by(|a, b| a.year.to_lowercase().cmp(&b.year.to_lowercase())), - EntryTableColumn::Pubtype => self - .entry_table_items - .sort_by(|a, b| a.pubtype.to_lowercase().cmp(&b.pubtype.to_lowercase())), - } - } - } -} - -// Define contents of each entry table row -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct EntryTableItem { - pub authors: String, - pub short_author: String, - pub title: String, - pub year: String, - pub pubtype: String, - pub keywords: String, - pub citekey: String, - pub abstract_text: String, - pub doi_url: String, - pub filepath: String, -} - -impl EntryTableItem { - // This functions decides which fields are rendered in the entry table - // Fields which should be usable but not visible can be left out - pub fn ref_vec(&mut self) -> Vec<&str> { - self.short_author = match self.authors.split_once(",") { - Some((first, _rest)) => { - if self.authors.contains("(ed.)") { - let first_author = format!("{} et al. (ed.)", first); - first_author - } else { - let first_author = format!("{} et al.", first); - first_author - } - } - None => String::from(""), - }; - - vec![ - { - if self.short_author.is_empty() { - &self.authors - } else { - &self.short_author - } - }, - &self.title, - &self.year, - &self.pubtype, - ] - } - - pub fn authors(&self) -> &str { - &self.authors - } - - pub fn title(&self) -> &str { - &self.title - } - - pub fn year(&self) -> &str { - &self.year - } - - pub fn pubtype(&self) -> &str { - &self.pubtype - } - - pub fn citekey(&self) -> &str { - &self.citekey - } - - pub fn doi_url(&self) -> &str { - &self.doi_url - } - - pub fn filepath(&self) -> &str { - &self.filepath - } -} - -impl App { - // Entry Table commands - - // Movement - pub fn select_next_entry(&mut self, entries: u16) { - self.entry_table.entry_info_scroll = 0; - self.entry_table.entry_info_scroll_state = - self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.scroll_down_by(entries); - self.entry_table.entry_scroll_state = self - .entry_table - .entry_scroll_state - .position(self.entry_table.entry_table_state.selected().unwrap()); - } - - pub fn select_previous_entry(&mut self, entries: u16) { - self.entry_table.entry_info_scroll = 0; - self.entry_table.entry_info_scroll_state = - self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.scroll_up_by(entries); - self.entry_table.entry_scroll_state = self - .entry_table - .entry_scroll_state - .position(self.entry_table.entry_table_state.selected().unwrap()); - } - - pub fn select_first_entry(&mut self) { - self.entry_table.entry_info_scroll = 0; - self.entry_table.entry_info_scroll_state = - self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.select_first(); - self.entry_table.entry_scroll_state = self.entry_table.entry_scroll_state.position(0); - } - - pub fn select_last_entry(&mut self) { - self.entry_table.entry_info_scroll = 0; - self.entry_table.entry_info_scroll_state = - self.entry_table.entry_info_scroll_state.position(0); - self.entry_table.entry_table_state.select_last(); - self.entry_table.entry_scroll_state = self - .entry_table - .entry_scroll_state - .position(self.entry_table.entry_table_items.len()); - } - - pub fn select_next_column(&mut self) { - match self.entry_table.entry_table_selected_column { - EntryTableColumn::Authors => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Title; - } - EntryTableColumn::Title => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Year; - } - EntryTableColumn::Year => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype; - } - EntryTableColumn::Pubtype => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Authors; - } - } - } - - pub fn select_prev_column(&mut self) { - match self.entry_table.entry_table_selected_column { - EntryTableColumn::Authors => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Pubtype; - } - EntryTableColumn::Title => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Authors; - } - EntryTableColumn::Year => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Title; - } - EntryTableColumn::Pubtype => { - self.entry_table.entry_table_selected_column = EntryTableColumn::Year; - } - } - } - - // Get the citekey of the selected entry - pub fn get_selected_citekey(&self) -> &str { - let idx = self.entry_table.entry_table_state.selected().unwrap(); - let citekey = &self.entry_table.entry_table_items[idx].citekey; - citekey - } - - pub fn run_editor(&mut self, tui: &mut Tui) -> Result<()> { - // get filecontent and citekey for calculating line number - let citekey = self.get_selected_citekey(); - // create independent copy of citekey for finding entry after updating list - let saved_key = citekey.to_owned(); - let filepath = self.main_biblio.bibfile.display().to_string(); - let filecontent = self.main_biblio.bibfilestring.clone(); - let mut line_count = 0; - - for line in filecontent.lines() { - line_count = line_count + 1; - // if reaching the citekey break the loop - // if reaching end of lines without match, reset to 0 - if line.contains(&citekey) { - break; - } else if line_count == filecontent.len() { - eprintln!( - "Citekey {} not found, opening file {} at line 1", - &citekey, &filepath - ); - line_count = 0; - break; - } - } - - // Exit TUI to enter editor - tui.exit()?; - // Use VISUAL or EDITOR. Set "vi" as last fallback - let mut cmd: Command = EditorBuilder::new() - .environment() - .source(Some("vi")) - .build() - .unwrap(); - // Prepare arguments to open file at specific line - let args: Vec = vec![format!("+{}", line_count), filepath]; - let status = cmd.args(&args).status()?; - if !status.success() { - eprintln!("Spawning editor failed with status {}", status); - } - - // Enter TUI again - tui.enter()?; - tui.terminal.clear()?; - - // Update the database and the lists to show changes - self.update_lists(); - - // Search for entry, selected before editing, by matching citekeys - // Use earlier saved copy of citekey to match - let mut idx_count = 0; - loop { - if self.entry_table.entry_table_items[idx_count] - .citekey - .contains(&saved_key) - { - break; - } - idx_count = idx_count + 1 - } - - // Set selected entry to vec-index of match - self.entry_table.entry_table_state.select(Some(idx_count)); - - Ok(()) - } - - // Search entry list - pub fn search_entries(&mut self) { - // Use snapshot of entry list saved when starting the search - // so deleting a char, will show former entries too - let orig_list = self.entry_table.entry_table_at_search_start.clone(); - let filtered_list = - BibiSearch::search_entry_list(&mut self.search_struct.search_string, orig_list.clone()); - self.entry_table.entry_table_items = filtered_list; - if self.entry_table.entry_table_reversed_sort { - self.entry_table.sort_entry_table(false); - } - self.entry_table.entry_scroll_state = ScrollbarState::content_length( - self.entry_table.entry_scroll_state, - self.entry_table.entry_table_items.len(), - ); - } - - // Open file connected with entry through 'file' or 'pdf' field - pub fn open_connected_file(&mut self) -> Result<()> { - let idx = self.entry_table.entry_table_state.selected().unwrap(); - let filepath = &self.entry_table.entry_table_items[idx].filepath.clone(); - - // Build command to execute pdf-reader. 'xdg-open' is Linux standard - let cmd = { - match std::env::consts::OS { - "linux" => String::from("xdg-open"), - "macos" => String::from("open"), - "windows" => String::from("start"), - _ => panic!("Couldn't detect OS for setting correct opener"), - } - }; - - // Pass filepath as argument, pipe stdout and stderr to /dev/null - // to keep the TUI clean (where is it piped on Windows???) - let _ = Command::new(&cmd) - .arg(&filepath) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .wrap_err("Opening file not possible"); - - Ok(()) - } - - pub fn open_doi_url(&mut self) -> Result<()> { - let idx = self.entry_table.entry_table_state.selected().unwrap(); - let web_adress = self.entry_table.entry_table_items[idx].doi_url.clone(); - - // Resolve strings using the resolving function of dx.doi.org, so the - // terminal is not blocked by the resolving process - let url = if web_adress.starts_with("10.") { - let prefix = "https://doi.org/".to_string(); - prefix + &web_adress - } else if web_adress.starts_with("www.") { - let prefix = "https://".to_string(); - prefix + &web_adress - } else { - web_adress - }; - - // Build command to execute browser. 'xdg-open' is Linux standard - let cmd = { - match std::env::consts::OS { - "linux" => String::from("xdg-open"), - "macos" => String::from("open"), - "windows" => String::from("start"), - _ => panic!("Couldn't detect OS for setting correct opener"), - } - }; - - // Pass filepath as argument, pipe stdout and stderr to /dev/null - // to keep the TUI clean (where is it piped on Windows???) - let _ = Command::new(&cmd) - .arg(url) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .wrap_err("Opening file not possible"); - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::EntryTableItem; - - #[test] - fn check_os() { - let os = std::env::consts::OS; - assert_eq!( - os, - "linux", - "You're not coding on linux, but on {}... Switch to linux, now!", - std::env::consts::OS - ) - } - - #[test] - fn shorten_authors() { - let mut entry: EntryTableItem = EntryTableItem { - authors: "Miller, Schmitz, Bernard".to_string(), - short_author: "".to_string(), - title: "A title".to_string(), - year: "2000".to_string(), - pubtype: "article".to_string(), - keywords: "key1, key2".to_string(), - citekey: "miller_2000".to_string(), - abstract_text: "An abstract".to_string(), - doi_url: "www.text.org".to_string(), - filepath: "/home/test".to_string(), - }; - - let entry_vec = EntryTableItem::ref_vec(&mut entry); - - let mut entry_editors: EntryTableItem = EntryTableItem { - authors: "Miller, Schmitz, Bernard (ed.)".to_string(), - short_author: "".to_string(), - title: "A title".to_string(), - year: "2000".to_string(), - pubtype: "article".to_string(), - keywords: "key1, key2".to_string(), - citekey: "miller_2000".to_string(), - abstract_text: "An abstract".to_string(), - doi_url: "www.text.org".to_string(), - filepath: "/home/test".to_string(), - }; - - let entry_vec_editors = EntryTableItem::ref_vec(&mut entry_editors); - - assert_eq!( - entry_vec, - vec!["Miller et al.", "A title", "2000", "article"] - ); - assert_eq!( - entry_vec_editors, - vec!["Miller et al. (ed.)", "A title", "2000", "article"] - ) - } -} diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs deleted file mode 100644 index 39ec7a2..0000000 --- a/src/frontend/handler.rs +++ /dev/null @@ -1,210 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use crate::frontend::app::App; -use crate::frontend::tui::Tui; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -use super::app::CurrentArea; -use color_eyre::eyre::Result; - -/// Handles the key events and updates the state of [`App`]. -pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> Result<()> { - // Keycodes activated for every area (high priority) - match key_event.code { - // Exit application on `ESC` or `q` - KeyCode::Char('Q') | KeyCode::Char('q') => { - app.quit(); - } - // Exit application on `Ctrl-C` - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.quit(); - } - } - KeyCode::PageDown => { - app.scroll_info_down(); - } - KeyCode::PageUp => { - app.scroll_info_up(); - } - _ => {} - } - // Keycodes for specific areas - match app.current_area { - // Keycodes for the tag area - CurrentArea::TagArea => match key_event.code { - KeyCode::Down => { - app.select_next_tag(1); - } - KeyCode::Up => { - app.select_previous_tag(1); - } - KeyCode::Char('j') => { - if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_down(); - } else { - app.select_next_tag(1); - } - } - KeyCode::Char('k') => { - if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_up(); - } else { - app.select_previous_tag(1); - } - } - KeyCode::Char('d') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.select_next_tag(5) - } - } - KeyCode::Char('u') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.select_previous_tag(5) - } - } - KeyCode::Char('g') | KeyCode::Home => { - app.select_first_tag(); - } - KeyCode::Char('G') | KeyCode::End => { - app.select_last_tag(); - } - KeyCode::Char('/') => { - app.enter_search_area(); - } - KeyCode::Char('f') | KeyCode::Char('F') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.enter_search_area(); - } - } - KeyCode::Tab | KeyCode::BackTab => { - app.toggle_area(); - } - KeyCode::Esc => { - app.reset_current_list(); - } - KeyCode::Enter => { - app.filter_for_tags(); - } - _ => {} - }, - // Keycodes for the entry area - CurrentArea::EntryArea => match key_event.code { - KeyCode::Down => { - app.select_next_entry(1); - } - KeyCode::Up => { - app.select_previous_entry(1); - } - KeyCode::Char('j') => { - if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_down(); - } else { - app.select_next_entry(1); - } - } - KeyCode::Char('k') => { - if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_up(); - } else { - app.select_previous_entry(1); - } - } - KeyCode::Char('d') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.select_next_entry(5); - } - } - KeyCode::Char('u') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.select_previous_entry(5); - } else { - app.open_doi_url()?; - } - } - KeyCode::Char('g') | KeyCode::Home => { - app.select_first_entry(); - } - KeyCode::Char('G') | KeyCode::End => { - app.select_last_entry(); - } - KeyCode::Char('h') => { - app.select_prev_column(); - } - KeyCode::Char('l') => { - app.select_next_column(); - } - KeyCode::Char('s') => { - app.entry_table.sort_entry_table(true); - } - KeyCode::Char('y') => { - App::yank_text(&app.get_selected_citekey()); - } - KeyCode::Char('e') => { - app.run_editor(tui)?; - } - KeyCode::Char('o') => { - app.open_connected_file()?; - } - KeyCode::Char('/') => { - app.enter_search_area(); - } - KeyCode::Char('f') | KeyCode::Char('F') => { - if key_event.modifiers == KeyModifiers::CONTROL { - app.enter_search_area(); - } - } - KeyCode::Tab | KeyCode::BackTab => { - app.toggle_area(); - } - KeyCode::Esc => { - app.reset_current_list(); - } - _ => {} - }, - // Keycodes for the search area (rendered in footer) - CurrentArea::SearchArea => match key_event.code { - KeyCode::Esc => { - app.break_search(); - } - KeyCode::Enter => { - app.confirm_search(); - } - KeyCode::Backspace => { - app.search_pattern_pop(); - } - KeyCode::Char(search_pattern) => { - app.search_pattern_push(search_pattern); - } - _ => {} - }, - // Keycodes for the help area (popup) - CurrentArea::HelpArea => match key_event.code { - KeyCode::Char('q') => { - app.quit(); - } - KeyCode::Esc => { - app.toggle_area(); - app.former_area = None; - } - _ => {} - }, - CurrentArea::InfoArea => {} - } - Ok(()) -} diff --git a/src/frontend/keywords.rs b/src/frontend/keywords.rs deleted file mode 100644 index 5605a59..0000000 --- a/src/frontend/keywords.rs +++ /dev/null @@ -1,159 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use super::app::{App, FormerArea}; -use crate::backend::search::BibiSearch; -use ratatui::widgets::{ListState, ScrollbarState}; - -#[derive(Debug)] -pub struct TagList { - pub tag_list_items: Vec, - pub tag_list_state: ListState, - pub tag_scroll_state: ScrollbarState, - pub selected_keywords: Vec, -} - -// Structure of the list items. -#[derive(Debug)] -pub struct TagListItem { - pub keyword: String, -} - -// Function to process inputed characters and convert them (to string, or more complex function) -impl TagListItem { - pub fn new(info: &str) -> Self { - Self { - keyword: info.to_string(), - } - } -} - -impl TagList { - pub fn new(keyword_list: Vec) -> Self { - let tag_list_items = keyword_list; - let tag_list_state = ListState::default(); // for preselection: .with_selected(Some(0)); - let tag_scroll_state = ScrollbarState::new(tag_list_items.len()); - Self { - tag_list_items, - tag_list_state, - tag_scroll_state, - selected_keywords: Vec::new(), - } - } -} - -impl App { - // Tag List commands - - // Movement - pub fn select_next_tag(&mut self, keywords: u16) { - self.tag_list.tag_list_state.scroll_down_by(keywords); - self.tag_list.tag_scroll_state = self - .tag_list - .tag_scroll_state - .position(self.tag_list.tag_list_state.selected().unwrap()); - } - - pub fn select_previous_tag(&mut self, keywords: u16) { - self.tag_list.tag_list_state.scroll_up_by(keywords); - self.tag_list.tag_scroll_state = self - .tag_list - .tag_scroll_state - .position(self.tag_list.tag_list_state.selected().unwrap()); - } - - pub fn select_first_tag(&mut self) { - self.tag_list.tag_list_state.select_first(); - self.tag_list.tag_scroll_state = self.tag_list.tag_scroll_state.position(0); - } - - pub fn select_last_tag(&mut self) { - self.tag_list.tag_list_state.select_last(); - self.tag_list.tag_scroll_state = self - .tag_list - .tag_scroll_state - .position(self.tag_list.tag_list_items.len()); - } - - pub fn get_selected_tag(&self) -> &str { - let idx = self.tag_list.tag_list_state.selected().unwrap(); - let keyword = &self.tag_list.tag_list_items[idx]; - // let keyword = &self.tag_list.tag_list_items[idx].keyword; - keyword - } - - pub fn search_tags(&mut self) { - let orig_list = &self.main_biblio.keyword_list; - let filtered_list = - BibiSearch::search_tag_list(&self.search_struct.search_string, orig_list.clone()); - self.tag_list.tag_list_items = filtered_list; - // Update scrollbar length after filtering list - self.tag_list.tag_scroll_state = ScrollbarState::content_length( - self.tag_list.tag_scroll_state, - self.tag_list.tag_list_items.len(), - ); - } - - pub fn filter_tags_by_entries(&mut self) { - let mut filtered_keywords: Vec = Vec::new(); - - let orig_list = &self.entry_table.entry_table_items; - - for e in orig_list { - if !e.keywords.is_empty() { - let mut key_vec: Vec = e - .keywords - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - filtered_keywords.append(&mut key_vec); - } - } - - filtered_keywords.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); - filtered_keywords.dedup(); - - self.search_struct.filtered_tag_list = filtered_keywords.clone(); - self.tag_list.tag_list_items = filtered_keywords; - self.tag_list.tag_scroll_state = ScrollbarState::content_length( - self.tag_list.tag_scroll_state, - self.tag_list.tag_list_items.len(), - ); - } - - // Filter the entry list by tags when hitting enter - // If already inside a filtered tag or entry list, apply the filtering - // to the already filtered list only - pub fn filter_for_tags(&mut self) { - let orig_list = &self.entry_table.entry_table_items; - let keyword = self.get_selected_tag(); - let filtered_list = BibiSearch::filter_entries_by_tag(&keyword, &orig_list); - // self.tag_list.selected_keyword = keyword.to_string(); - self.tag_list.selected_keywords.push(keyword.to_string()); - self.entry_table.entry_table_items = filtered_list; - // Update scrollbar state with new lenght of itemlist - self.entry_table.entry_scroll_state = ScrollbarState::content_length( - self.entry_table.entry_scroll_state, - self.entry_table.entry_table_items.len(), - ); - self.filter_tags_by_entries(); - self.toggle_area(); - self.entry_table.entry_table_state.select(Some(0)); - self.former_area = Some(FormerArea::TagArea); - } -} diff --git a/src/frontend/tui.rs b/src/frontend/tui.rs deleted file mode 100644 index e3c9c1a..0000000 --- a/src/frontend/tui.rs +++ /dev/null @@ -1,223 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use crate::frontend::app::App; -use crossterm::{ - cursor, - event::{ - DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, KeyEvent, MouseEvent, - }, - terminal::{EnterAlternateScreen, LeaveAlternateScreen}, -}; -// use ratatui::backend::{Backend, CrosstermBackend}; -use color_eyre::eyre::{OptionExt, Result}; -use futures::{FutureExt, StreamExt}; -use ratatui::backend::CrosstermBackend as Backend; -use std::io::{stdout, Stdout}; -use std::panic; -use std::{ - ops::{Deref, DerefMut}, - time::Duration, -}; -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; - -// Terminal events. -#[derive(Clone, Copy, Debug)] -pub enum Event { - /// Terminal tick. - Tick, - /// Key press. - Key(KeyEvent), - /// Mouse click/scroll. - Mouse(MouseEvent), - /// Terminal resize. - Resize(u16, u16), -} - -#[derive(Debug)] -pub struct Tui { - /// Interface to the Terminal. - pub terminal: ratatui::Terminal>, - /// Event sender channel. - sender: mpsc::UnboundedSender, - /// Event receiver channel. - receiver: mpsc::UnboundedReceiver, - /// Event handler thread. - handler: tokio::task::JoinHandle<()>, - cancellation_token: CancellationToken, -} - -impl Tui { - // Constructs a new instance of [`Tui`]. - pub fn new() -> Result { - let terminal = ratatui::Terminal::new(Backend::new(stdout()))?; - let (sender, receiver) = mpsc::unbounded_channel(); - let handler = tokio::spawn(async {}); - let cancellation_token = CancellationToken::new(); - Ok(Self { - terminal, - sender, - receiver, - handler, - cancellation_token, - }) - } - - pub fn start(&mut self) { - let tick_rate = Duration::from_millis(1000); - self.cancel(); - self.cancellation_token = CancellationToken::new(); - let event_loop = Self::event_loop( - self.sender.clone(), - self.cancellation_token.clone(), - tick_rate, - ); - // let _cancellation_token = self.cancellation_token.clone(); - // let _sender = self.sender.clone(); - self.handler = tokio::spawn(async { - event_loop.await; - }); - } - - async fn event_loop( - sender: mpsc::UnboundedSender, - cancellation_token: CancellationToken, - tick_rate: Duration, - ) { - let mut reader = crossterm::event::EventStream::new(); - let mut tick = tokio::time::interval(tick_rate); - loop { - let tick_delay = tick.tick(); - let crossterm_event = reader.next().fuse(); - tokio::select! { - // _ = sender.closed() => { - // break; - // } - _ = cancellation_token.cancelled() => { - break; - } - Some(Ok(evt)) = crossterm_event => { - match evt { - CrosstermEvent::Key(key) => { - if key.kind == crossterm::event::KeyEventKind::Press { - sender.send(Event::Key(key)).unwrap(); - } - }, - CrosstermEvent::Mouse(mouse) => { - sender.send(Event::Mouse(mouse)).unwrap(); - }, - CrosstermEvent::Resize(x, y) => { - sender.send(Event::Resize(x, y)).unwrap(); - }, - CrosstermEvent::FocusLost => { - }, - CrosstermEvent::FocusGained => { - }, - CrosstermEvent::Paste(_) => { - }, - } - } - _ = tick_delay => { - sender.send(Event::Tick).unwrap(); - } - }; - } - cancellation_token.cancel(); - } - - pub fn enter(&mut self) -> Result<()> { - crossterm::terminal::enable_raw_mode()?; - crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?; - // if self.mouse { - crossterm::execute!(stdout(), EnableMouseCapture)?; - // } - // if self.paste { - // crossterm::execute!(stdout(), EnableBracketedPaste)?; - // } - // Self::init_error_hooks()?; - self.start(); - Ok(()) - } - - pub fn cancel(&self) { - self.cancellation_token.cancel(); - } - - pub fn suspend(&mut self) -> Result<()> { - self.exit()?; - #[cfg(not(windows))] - signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; - Ok(()) - } - - pub fn resume(&mut self) -> Result<()> { - self.enter()?; - Ok(()) - } - - pub fn exit(&mut self) -> Result<()> { - self.cancellation_token.cancel(); - if crossterm::terminal::is_raw_mode_enabled()? { - self.flush()?; - // if self.paste { - // crossterm::execute!(stdout(), DisableBracketedPaste)?; - // } - // if self.mouse { - crossterm::execute!(stdout(), DisableMouseCapture)?; - // } - crossterm::execute!(stdout(), LeaveAlternateScreen, cursor::Show)?; - crossterm::terminal::disable_raw_mode()?; - } - Ok(()) - } - - // [`Draw`] the terminal interface by [`rendering`] the widgets. - // - // [`Draw`]: ratatui::Terminal::draw - // [`rendering`]: crate::ui::render - pub fn draw(&mut self, app: &mut App) -> Result<()> { - // self.terminal.draw(|frame| ui::render(app, frame))?; - self.terminal - .draw(|frame| frame.render_widget(app, frame.area()))?; - Ok(()) - } - - pub async fn next(&mut self) -> Result { - self.receiver.recv().await.ok_or_eyre("This is an IO error") - } -} - -impl Deref for Tui { - type Target = ratatui::Terminal>; - - fn deref(&self) -> &Self::Target { - &self.terminal - } -} - -impl DerefMut for Tui { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.terminal - } -} - -impl Drop for Tui { - fn drop(&mut self) { - self.exit().unwrap(); - } -} diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs deleted file mode 100644 index 45ccd60..0000000 --- a/src/frontend/ui.rs +++ /dev/null @@ -1,646 +0,0 @@ -// bibiman - a TUI for managing BibLaTeX databases -// Copyright (C) 2024 lukeflo -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -///// - -use color_eyre::owo_colors::OwoColorize; -use itertools::Itertools; -use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Rect}, - style::{Color, Modifier, Style, Stylize}, - symbols, - text::{Line, Span, Text}, - widgets::{ - block::{Position, Title}, - Block, Borders, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, Scrollbar, - ScrollbarOrientation, StatefulWidget, Table, Widget, Wrap, - }, -}; - -use crate::frontend::{app::App, keywords::TagListItem}; - -use super::{ - app::{CurrentArea, FormerArea}, - entries::EntryTableColumn, - keywords, -}; - -const MAIN_BLUE_COLOR: Color = Color::Indexed(39); -// const MAIN_PURPLE_COLOR: Color = Color::Indexed(129); -const BOX_SELECTED_BOX_STYLE: Style = Style::new().fg(TEXT_FG_COLOR); -const BOX_SELECTED_TITLE_STYLE: Style = Style::new().fg(TEXT_FG_COLOR).add_modifier(Modifier::BOLD); -const BOX_UNSELECTED_BORDER_STYLE: Style = Style::new().fg(TEXT_UNSELECTED_FG_COLOR); -const BOX_UNSELECTED_TITLE_STYLE: Style = Style::new() - .fg(TEXT_UNSELECTED_FG_COLOR) - .add_modifier(Modifier::BOLD); -const NORMAL_ROW_BG: Color = Color::Black; -const ALT_ROW_BG_COLOR: Color = Color::Indexed(234); -const SELECTED_STYLE: Style = Style::new() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED); -const TEXT_FG_COLOR: Color = Color::Indexed(252); -const TEXT_UNSELECTED_FG_COLOR: Color = Color::Indexed(245); -const SORTED_ENTRIES: &str = "▼"; -const SORTED_ENTRIES_REVERSED: &str = "▲"; -const HEADER_FOOTER_BG: Color = Color::Indexed(235); - -const SCROLLBAR_UPPER_CORNER: Option<&str> = Some("┓"); -const SCROLLBAR_LOWER_CORNER: Option<&str> = Some("┛"); - -pub const fn alternate_colors(i: usize) -> Color { - if i % 2 == 0 { - NORMAL_ROW_BG - } else { - ALT_ROW_BG_COLOR - } -} - -impl From<&TagListItem> for ListItem<'_> { - fn from(value: &TagListItem) -> Self { - let line = Line::styled(format!("{}", value.keyword), TEXT_FG_COLOR); - ListItem::new(line) - } -} - -impl Widget for &mut App { - fn render(self, area: Rect, buf: &mut Buffer) { - let [header_area, main_area, footer_area] = Layout::vertical([ - Constraint::Length(1), - Constraint::Fill(1), - Constraint::Length(3), - ]) - .areas(area); - - let [list_area, item_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area); - - let [entry_area, entry_info_area] = - Layout::vertical([Constraint::Fill(1), Constraint::Length(2)]).areas(list_area); - - let [tag_area, info_area] = - Layout::horizontal([Constraint::Max(25), Constraint::Min(35)]).areas(item_area); - - // Render header and footer - App::render_header(header_area, buf); - self.render_footer(footer_area, buf); - // Render list area where entry gets selected - self.render_entrytable(entry_area, buf); - self.render_file_info(entry_info_area, buf); - // Render infos related to selected entry - self.render_taglist(tag_area, buf); - self.render_selected_item(info_area, buf); - } -} - -impl App { - pub fn render_header(area: Rect, buf: &mut Buffer) { - Paragraph::new("BIBIMAN – BibLaTeX manager TUI") - .bold() - .fg(MAIN_BLUE_COLOR) - .centered() - .render(area, buf); - } - - pub fn render_footer(&mut self, area: Rect, buf: &mut Buffer) { - match &self.current_area { - CurrentArea::SearchArea => { - let search_title = { - match self.former_area { - Some(FormerArea::EntryArea) => { - let search_title = " Search Entries ".to_string(); - search_title - } - Some(FormerArea::TagArea) => { - let search_title = " Search Keywords ".to_string(); - search_title - } - _ => { - let search_title = " Search ".to_string(); - search_title - } - } - }; - - let block = Block::bordered() - .title(Line::styled(search_title, BOX_SELECTED_TITLE_STYLE)) - .border_style(BOX_SELECTED_BOX_STYLE) - .border_set(symbols::border::THICK); - Paragraph::new(self.search_struct.search_string.clone()) - .block(block) - .render(area, buf); - } - _ => { - let style_emph = Style::new().bold().fg(TEXT_FG_COLOR); - let block = Block::bordered() - .title(Line::raw(" Basic Commands ").centered()) - .border_style(BOX_UNSELECTED_BORDER_STYLE) - .border_set(symbols::border::PLAIN); - Paragraph::new(Line::from(vec![ - Span::styled("j/k: ", style_emph), - Span::raw("move | "), - Span::styled("g/G: ", style_emph), - Span::raw("top/bottom | "), - Span::styled("TAB: ", style_emph), - Span::raw("switch tab | "), - Span::styled("y: ", style_emph), - Span::raw("yank citekey | "), - Span::styled("e: ", style_emph), - Span::raw("edit | "), - Span::styled("/: ", style_emph), - Span::raw("search | "), - Span::styled("o/u: ", style_emph), - Span::raw("open PDF/DOI"), - ])) - .block(block) - .centered() - .render(area, buf); - } - } - } - - // Render info of the current file and process - // 1. Basename of the currently loaded file - // 2. Keyword by which the entries are filtered at the moment - // 3. Currently selected entry and total count of entries - pub fn render_file_info(&mut self, area: Rect, buf: &mut Buffer) { - let block = Block::new() // can also be Block::new - // Leave Top empty to simulate one large box with borders of entry list - .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) - .border_set(if let CurrentArea::EntryArea = self.current_area { - symbols::border::THICK - } else { - symbols::border::PLAIN - }) - .border_style(if let CurrentArea::EntryArea = self.current_area { - BOX_SELECTED_BOX_STYLE - } else { - BOX_UNSELECTED_BORDER_STYLE - }); - - let [file_area, keyword_area, count_area] = Layout::horizontal([ - Constraint::Fill(3), - Constraint::Fill(4), - Constraint::Fill(1), - ]) - .horizontal_margin(1) - .areas(area); - - Line::from(vec![ - Span::raw("File: ").bold(), - Span::raw(self.main_bibfile.file_name().unwrap().to_string_lossy()).bold(), - ]) - .bg(HEADER_FOOTER_BG) - .render(file_area, buf); - - Line::from(if !self.tag_list.selected_keywords.is_empty() { - vec![ - Span::raw("Selected keywords: "), - // Show all keywords in correct order if list is filtered - // successively by multiple keywords - Span::raw(self.tag_list.selected_keywords.join(" → ")) - .bold() - .green(), - ] - } else { - vec![Span::raw(" ")] - }) - .bg(HEADER_FOOTER_BG) - .render(keyword_area, buf); - - Line::from(if self.entry_table.entry_table_state.selected().is_some() { - vec![ - Span::raw((self.entry_table.entry_table_state.selected().unwrap() + 1).to_string()) - .bold(), - Span::raw("/"), - Span::raw(self.entry_table.entry_table_items.len().to_string()), - ] - } else { - vec![Span::raw("No entries")] - }) - .right_aligned() - .bg(HEADER_FOOTER_BG) - .render(count_area, buf); - - // Render that stuff - Widget::render(block, area, buf) - } - - pub fn render_entrytable(&mut self, area: Rect, buf: &mut Buffer) { - let block = Block::new() // can also be Block::new - .title( - Line::styled( - " Bibliographic Entries ", - if let CurrentArea::EntryArea = self.current_area { - BOX_SELECTED_TITLE_STYLE - } else { - BOX_UNSELECTED_TITLE_STYLE - }, - ) - .centered(), - ) - .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP) - .border_set(if let CurrentArea::EntryArea = self.current_area { - symbols::border::THICK - } else { - symbols::border::PLAIN - }) - .border_style(if let CurrentArea::EntryArea = self.current_area { - BOX_SELECTED_BOX_STYLE - } else { - BOX_UNSELECTED_BORDER_STYLE - }); - - let header_style = Style::default() - .bold() - .fg(TEXT_FG_COLOR) - .bg(HEADER_FOOTER_BG); - - let header_selected_col = Style::default().underlined(); - - let header = Row::new(vec![ - Cell::from(Line::from(vec![ - { - if let EntryTableColumn::Authors = self.entry_table.entry_table_selected_column - { - Span::styled("Author", header_selected_col) - } else { - Span::raw("Author") - } - }, - { - if let EntryTableColumn::Authors = self.entry_table.entry_table_sorted_by_col { - Span::raw(format!( - " {}", - if self.entry_table.entry_table_reversed_sort { - SORTED_ENTRIES_REVERSED - } else { - SORTED_ENTRIES - } - )) - } else { - Span::raw("") - } - }, - ])), - Cell::from(Line::from(vec![ - { - if let EntryTableColumn::Title = self.entry_table.entry_table_selected_column { - Span::styled("Title", header_selected_col) - } else { - Span::raw("Title") - } - }, - { - if let EntryTableColumn::Title = self.entry_table.entry_table_sorted_by_col { - Span::raw(format!( - " {}", - if self.entry_table.entry_table_reversed_sort { - SORTED_ENTRIES_REVERSED - } else { - SORTED_ENTRIES - } - )) - } else { - Span::raw("") - } - }, - ])), - Cell::from(Line::from(vec![ - { - if let EntryTableColumn::Year = self.entry_table.entry_table_selected_column { - Span::styled("Year", header_selected_col) - } else { - Span::raw("Year") - } - }, - { - if let EntryTableColumn::Year = self.entry_table.entry_table_sorted_by_col { - Span::raw(format!( - " {}", - if self.entry_table.entry_table_reversed_sort { - SORTED_ENTRIES_REVERSED - } else { - SORTED_ENTRIES - } - )) - } else { - Span::raw("") - } - }, - ])), - Cell::from(Line::from(vec![ - { - if let EntryTableColumn::Pubtype = self.entry_table.entry_table_selected_column - { - Span::styled("Pubtype", header_selected_col) - } else { - Span::raw("Pubtype") - } - }, - { - if let EntryTableColumn::Pubtype = self.entry_table.entry_table_sorted_by_col { - Span::raw(format!( - " {}", - if self.entry_table.entry_table_reversed_sort { - SORTED_ENTRIES_REVERSED - } else { - SORTED_ENTRIES - } - )) - } else { - Span::raw("") - } - }, - ])), - ]) - .style(header_style) - .height(1); - - // Iterate over vector storing each entries data fields - let rows = self - .entry_table - .entry_table_items - .iter_mut() - .enumerate() - .map(|(_i, data)| { - let item = data.ref_vec(); - item.into_iter() - .map(|content| Cell::from(Text::from(format!("{content}")))) - .collect::() - .style(Style::new().fg(TEXT_FG_COLOR)) //.bg(alternate_colors(i))) - .height(1) - }); - let entry_table = Table::new( - rows, - [ - Constraint::Percentage(20), - Constraint::Fill(1), - Constraint::Length( - if let EntryTableColumn::Year = self.entry_table.entry_table_sorted_by_col { - 6 - } else { - 4 - }, - ), - Constraint::Percentage(10), - ], - ) - .block(block) - .header(header) - .column_spacing(2) - .highlight_style(SELECTED_STYLE) - // .bg(Color::Black) - .highlight_spacing(HighlightSpacing::Always); - StatefulWidget::render( - entry_table, - area, - buf, - &mut self.entry_table.entry_table_state, - ); - - // Scrollbar for entry table - let scrollbar = Scrollbar::default() - .orientation(ScrollbarOrientation::VerticalRight) - .track_symbol(None) - .begin_symbol(SCROLLBAR_UPPER_CORNER) - .end_symbol(None) - .thumb_style(Style::new().fg(Color::DarkGray)); - - if let CurrentArea::EntryArea = self.current_area { - // render the scrollbar - StatefulWidget::render( - scrollbar, - area, - buf, - &mut self.entry_table.entry_scroll_state, - ); - } - } - - pub fn render_selected_item(&mut self, area: Rect, buf: &mut Buffer) { - // We get the info depending on the item's state. - let style_value = Style::new().bold().fg(TEXT_FG_COLOR); - let style_value_sec = Style::new() - .add_modifier(Modifier::ITALIC) - .fg(TEXT_FG_COLOR); - let lines = { - // if self.entry_table.entry_table_items.len() > 0 { - if self.entry_table.entry_table_state.selected().is_some() { - let idx = self.entry_table.entry_table_state.selected().unwrap(); - let cur_entry = &self.entry_table.entry_table_items[idx]; - let mut lines = vec![]; - lines.push(Line::from(vec![ - Span::styled("Authors: ", style_value), - // Span::styled(cur_entry.authors.clone(), Style::new().green()), - Span::styled(cur_entry.authors(), Style::new().green()), - ])); - lines.push(Line::from(vec![ - Span::styled("Title: ", style_value), - Span::styled(cur_entry.title(), Style::new().magenta()), - ])); - lines.push(Line::from(vec![ - Span::styled("Year: ", style_value), - Span::styled(cur_entry.year(), Style::new().light_magenta()), - ])); - // Render keywords in info box in Markdown code style - if !cur_entry.keywords.is_empty() { - let kw: Vec<&str> = cur_entry - .keywords - .split(",") - .map(|k| k.trim()) - .filter(|k| !k.is_empty()) - .collect(); - let mut content = vec![Span::styled("Keywords: ", style_value)]; - for k in kw { - // Add half block highlighted in bg color to enlarge block - content.push(Span::raw("▐").fg(HEADER_FOOTER_BG)); - content.push(Span::styled( - k, - Style::default().bg(HEADER_FOOTER_BG).fg( - // Highlight selected keyword green - if self.tag_list.selected_keywords.iter().any(|e| e == k) { - Color::Green - } else { - TEXT_FG_COLOR - }, - ), - )); - content.push(Span::raw("▌").fg(HEADER_FOOTER_BG)); - } - lines.push(Line::from(content)) - } - if !cur_entry.doi_url.is_empty() || !cur_entry.filepath.is_empty() { - lines.push(Line::raw("")); - } - if !cur_entry.doi_url.is_empty() { - lines.push(Line::from(vec![ - Span::styled("DOI/URL: ", style_value_sec), - Span::styled( - cur_entry.doi_url(), - Style::default().fg(TEXT_FG_COLOR).underlined(), - ), - ])); - } - if !cur_entry.filepath.is_empty() { - lines.push(Line::from(vec![ - Span::styled("File: ", style_value_sec), - Span::styled(cur_entry.filepath(), Style::default().fg(TEXT_FG_COLOR)), - ])); - } - lines.push(Line::from("")); - lines.push(Line::from(vec![Span::styled( - cur_entry.abstract_text.clone(), - Style::default().fg(TEXT_FG_COLOR), - )])); - lines - } else { - let lines = vec![ - Line::from(" "), - Line::from("No entry selected".bold().into_centered_line().red()), - ]; - lines - } - }; - let info = Text::from(lines); - - // We show the list item's info under the list in this paragraph - let block = Block::bordered() - .title(Line::raw(" Entry Information ").centered().bold()) - // .borders(Borders::TOP) - .border_set(symbols::border::PLAIN) - .border_style(BOX_UNSELECTED_BORDER_STYLE) - // .bg(Color::Black) - .padding(Padding::horizontal(1)); - - // INFO: '.line_count' method only possible with unstable-rendered-line-info feature -> API might change: https://github.com/ratatui/ratatui/issues/293#ref-pullrequest-2027056434 - let box_height = Paragraph::new(info.clone()) - .block(block.clone()) - .wrap(Wrap { trim: false }) - .line_count(area.width); - // Make sure to allow scroll only if text is larger than the rendered area and stop scrolling when last line is reached - let scroll_height = { - if self.entry_table.entry_info_scroll == 0 { - self.entry_table.entry_info_scroll - } else if area.height > box_height as u16 { - self.entry_table.entry_info_scroll = 0; - self.entry_table.entry_info_scroll - } else if self.entry_table.entry_info_scroll > (box_height as u16 + 2 - area.height) { - self.entry_table.entry_info_scroll = box_height as u16 + 2 - area.height; - self.entry_table.entry_info_scroll - } else { - self.entry_table.entry_info_scroll - } - }; - - // We can now render the item info - Paragraph::new(info) - .block( - block - // Render arrows to show that info box has content outside the block - .title( - Title::from( - if box_height > area.height.into() - && self.entry_table.entry_info_scroll - < box_height as u16 + 2 - area.height - { - " ▼ " - } else { - "" - }, - ) - .position(Position::Bottom) - .alignment(Alignment::Right), - ) - .title( - Title::from(if scroll_height > 0 { " ▲ " } else { "" }) - .position(Position::Top) - .alignment(Alignment::Right), - ), - ) - // .fg(TEXT_FG_COLOR) - .wrap(Wrap { trim: false }) - .scroll((scroll_height, 0)) - .render(area, buf); - } - - pub fn render_taglist(&mut self, area: Rect, buf: &mut Buffer) { - let block = Block::bordered() - .title( - Line::styled( - " Keywords ", - if let CurrentArea::TagArea = self.current_area { - BOX_SELECTED_TITLE_STYLE - } else { - BOX_UNSELECTED_TITLE_STYLE - }, - ) - .centered(), - ) - .border_set(if let CurrentArea::TagArea = self.current_area { - symbols::border::THICK - } else { - symbols::border::PLAIN - }) - .border_style(if let CurrentArea::TagArea = self.current_area { - BOX_SELECTED_BOX_STYLE - } else { - BOX_UNSELECTED_BORDER_STYLE - }); - // .bg(Color::Black); - - // Iterate through all elements in the `items` and stylize them. - let items: Vec = self - .tag_list - .tag_list_items - .iter() - .enumerate() - .map(|(_i, todo_item)| { - // let color = alternate_colors(i); - ListItem::from(todo_item.to_owned()) //.bg(color) - }) - .collect(); - - // Create a List from all list items and highlight the currently selected one - let list = List::new(items) - .block(block) - .highlight_style(SELECTED_STYLE) - // .highlight_symbol("> ") - .highlight_spacing(HighlightSpacing::Always); - - // Save list length for calculating scrollbar need - // Add 2 to compmensate lines of the block border - let list_length = list.len() + 2; - - // We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the - // same method name `render`. - StatefulWidget::render(list, area, buf, &mut self.tag_list.tag_list_state); - - // Scrollbar for keyword list - let scrollbar = Scrollbar::default() - .orientation(ScrollbarOrientation::VerticalRight) - .track_symbol(None) - .begin_symbol(SCROLLBAR_UPPER_CORNER) - .end_symbol(SCROLLBAR_LOWER_CORNER) - .thumb_style(Style::new().fg(Color::DarkGray)); - - if list_length > area.height.into() { - if let CurrentArea::TagArea = self.current_area { - // render the scrollbar - StatefulWidget::render(scrollbar, area, buf, &mut self.tag_list.tag_scroll_state); - } - } - } -} -- cgit v1.2.3