From f32b6a19851b8b103ac843503ab008197f0639cd Mon Sep 17 00:00:00 2001 From: lukeflo Date: Thu, 24 Oct 2024 12:53:09 +0200 Subject: rearrange code again, prepare for command-action setup --- Cargo.lock | 34 +++++- Cargo.toml | 1 + src/app.rs | 75 +++++++++++++ src/bib.rs | 21 ---- src/bib/bibmain.rs | 279 ----------------------------------------------- src/bib/entries.rs | 258 ------------------------------------------- src/bib/keywords.rs | 55 ---------- src/bib/search.rs | 136 ----------------------- src/bibiman.rs | 223 +++++++++++++++++++++++++++++++++++++ src/bibiman/bibisetup.rs | 279 +++++++++++++++++++++++++++++++++++++++++++++++ src/bibiman/entries.rs | 258 +++++++++++++++++++++++++++++++++++++++++++ src/bibiman/keywords.rs | 55 ++++++++++ src/bibiman/search.rs | 136 +++++++++++++++++++++++ src/main.rs | 5 +- src/tui.rs | 21 ++-- src/tui/app.rs | 257 ------------------------------------------- src/tui/command.rs | 10 +- src/tui/commandnew.rs | 178 ++++++++++++++++++++++++++++++ src/tui/handler.rs | 97 ++++++++-------- src/tui/ui.rs | 22 ++-- 20 files changed, 1317 insertions(+), 1083 deletions(-) create mode 100644 src/app.rs delete mode 100644 src/bib.rs delete mode 100644 src/bib/bibmain.rs delete mode 100644 src/bib/entries.rs delete mode 100644 src/bib/keywords.rs delete mode 100644 src/bib/search.rs create mode 100644 src/bibiman.rs create mode 100644 src/bibiman/bibisetup.rs create mode 100644 src/bibiman/entries.rs create mode 100644 src/bibiman/keywords.rs create mode 100644 src/bibiman/search.rs delete mode 100644 src/tui/app.rs create mode 100644 src/tui/commandnew.rs diff --git a/Cargo.lock b/Cargo.lock index 156a27c..496ba45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,11 +82,12 @@ dependencies = [ "hayagriva", "itertools", "nucleo-matcher", - "ratatui", + "ratatui 0.29.0", "sarge", "signal-hook", "tokio", "tokio-util", + "tui-input", ] [[package]] @@ -1131,6 +1132,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" +dependencies = [ + "bitflags 2.6.0", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.1.14", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -1539,6 +1561,16 @@ dependencies = [ "petgraph", ] +[[package]] +name = "tui-input" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" +dependencies = [ + "ratatui 0.28.1", + "unicode-width 0.1.14", +] + [[package]] name = "unic-langid" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 1346a21..194aeda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,4 @@ sarge = "7.2.5" signal-hook = "0.3.17" tokio = { version = "1.39.3", features = ["full"] } tokio-util = "0.7.12" +tui-input = "0.10.1" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e3ba079 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,75 @@ +// 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::Event; +use crate::bibiman::Bibiman; +use crate::cliargs::CLIArgs; +use crate::tui; +use crate::tui::handler::handle_key_events; +use color_eyre::eyre::{Ok, Result}; +use tui::Event; + +// Application. +#[derive(Debug)] +pub struct App { + // Is the application running? + pub running: bool, + // bibimain + pub bibiman: Bibiman, +} + +impl App { + // Constructs a new instance of [`App`]. + pub fn new(args: CLIArgs) -> Result { + // Self::default() + let running = true; + let bibiman = Bibiman::new(args)?; + Ok(Self { running, bibiman }) + } + + 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; + } +} diff --git a/src/bib.rs b/src/bib.rs deleted file mode 100644 index 8443b9a..0000000 --- a/src/bib.rs +++ /dev/null @@ -1,21 +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 . -///// - -pub mod bibmain; -pub mod entries; -pub mod keywords; -pub mod search; diff --git a/src/bib/bibmain.rs b/src/bib/bibmain.rs deleted file mode 100644 index a7df951..0000000 --- a/src/bib/bibmain.rs +++ /dev/null @@ -1,279 +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 biblatex::{self, Bibliography}; -use biblatex::{ChunksExt, Type}; -use itertools::Itertools; -use std::{fs, path::PathBuf}; - -#[derive(Debug)] -pub enum FileFormat { - BibLatex, - Hayagriva, -} - -// Set necessary fields -// TODO: can surely be made more efficient/simpler -#[derive(Debug)] -pub struct BibiMain { - pub bibfile: PathBuf, // path to bibfile - pub bibfile_format: FileFormat, // Format of passed file - pub bibfilestring: String, // content of bibfile as string - pub bibliography: Bibliography, // parsed bibliography - pub citekeys: Vec, // list of all citekeys - pub keyword_list: Vec, // list of all available keywords - pub entry_list: Vec, // List of all entries -} - -#[derive(Debug, Clone)] -pub struct BibiData { - pub authors: 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 BibiMain { - pub fn new(main_bibfile: PathBuf) -> Self { - // TODO: Needs check for config file path as soon as config file is impl - let bibfile_format = Self::check_file_format(&main_bibfile); - let bibfile = main_bibfile; - let bibfilestring = fs::read_to_string(&bibfile).unwrap(); - let bibliography = biblatex::Bibliography::parse(&bibfilestring).unwrap(); - let citekeys = Self::get_citekeys(&bibliography); - let keyword_list = Self::collect_tag_list(&citekeys, &bibliography); - let entry_list = Self::create_entry_list(&citekeys, &bibliography); - Self { - bibfile, - bibfile_format, - bibfilestring, - bibliography, - citekeys, - keyword_list, - entry_list, - } - } - - // Check which file format the passed file has - fn check_file_format(main_bibfile: &PathBuf) -> FileFormat { - let extension = main_bibfile.extension().unwrap().to_str(); - - match extension { - Some("yml") => FileFormat::Hayagriva, - Some("yaml") => FileFormat::Hayagriva, - Some("bib") => FileFormat::BibLatex, - Some(_) => panic!("The extension {:?} is no valid bibfile", extension.unwrap()), - None => panic!("The given path {:?} holds no valid file", main_bibfile), - } - } - - fn create_entry_list(citekeys: &[String], bibliography: &Bibliography) -> Vec { - citekeys - .into_iter() - .map(|k| BibiData { - authors: Self::get_authors(&k, &bibliography), - title: Self::get_title(&k, &bibliography), - year: Self::get_year(&k, &bibliography), - pubtype: Self::get_pubtype(&k, &bibliography), - keywords: Self::get_keywords(&k, &bibliography), - citekey: k.to_owned(), - abstract_text: Self::get_abstract(&k, &bibliography), - doi_url: Self::get_weblink(&k, &bibliography), - filepath: Self::get_filepath(&k, &bibliography), - }) - .collect() - } - - // get list of citekeys from the given bibfile - // this list is the base for further operations on the bibentries - // since it is the entry point of the biblatex crate. - pub fn get_citekeys(bibstring: &Bibliography) -> Vec { - let citekeys: Vec = bibstring.keys().map(|k| k.to_owned()).collect(); - citekeys - } - - // collect all keywords present in the bibliography - // sort them and remove duplicates - // this list is for fast filtering entries by topics/keyowrds - pub fn collect_tag_list(citekeys: &[String], biblio: &Bibliography) -> Vec { - // Initialize vector collecting all keywords - let mut keyword_list = vec![]; - - // Loop over entries and collect all keywords - for i in citekeys { - if biblio.get(&i).unwrap().keywords().is_ok() { - let items = biblio - .get(&i) - .unwrap() - .keywords() - .unwrap() - .format_verbatim(); - // Split keyword string into slices, trim leading and trailing - // whitespaces, remove empty slices, and collect them - let mut key_vec: Vec = items - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - // Append keywords to vector - keyword_list.append(&mut key_vec); - } - } - - // Sort the vector and remove duplicates - keyword_list.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); - keyword_list.dedup(); - keyword_list - } - - pub fn get_authors(citekey: &str, biblio: &Bibliography) -> String { - if biblio.get(&citekey).unwrap().author().is_ok() { - let authors = biblio.get(&citekey).unwrap().author().unwrap(); - if authors.len() > 1 { - let all_authors = authors.iter().map(|a| &a.name).join(", "); - all_authors - } else if authors.len() == 1 { - let authors = authors[0].name.to_string(); - authors - } else { - let editors_authors = format!("empty"); - editors_authors - } - } else { - if !biblio.get(&citekey).unwrap().editors().unwrap().is_empty() { - let editors = biblio.get(&citekey).unwrap().editors().unwrap(); - if editors[0].0.len() > 1 { - // let editors = format!("{} (ed.) et al.", editors[0].0[0].name); - let mut editors = editors[0].0.iter().map(|e| &e.name).join(", "); - editors.push_str(" (ed.)"); - editors - } else if editors[0].0.len() == 1 { - let editors = format!("{} (ed.)", editors[0].0[0].name); - editors - } else { - let editors_authors = format!("empty"); - editors_authors - } - } else { - let editors_authors = format!("empty"); - editors_authors - } - } - } - - pub fn get_title(citekey: &str, biblio: &Bibliography) -> String { - let title = { - if biblio.get(&citekey).unwrap().title().is_ok() { - let title = biblio - .get(&citekey) - .unwrap() - .title() - .unwrap() - .format_verbatim(); - title - } else { - let title = format!("no title"); - title - } - }; - title - } - - pub fn get_year(citekey: &str, biblio: &Bibliography) -> String { - let year = biblio.get(&citekey).unwrap(); - let year = { - if year.date().is_ok() { - let year = year.date().unwrap().to_chunks().format_verbatim(); - let year = year[..4].to_string(); - year - } else { - let year = format!("n.d."); - year - } - }; - year - } - - pub fn get_pubtype(citekey: &str, biblio: &Bibliography) -> String { - let pubtype = biblio.get(&citekey).unwrap().entry_type.to_string(); - pubtype - } - - pub fn get_keywords(citekey: &str, biblio: &Bibliography) -> String { - let keywords = { - if biblio.get(&citekey).unwrap().keywords().is_ok() { - let keywords = biblio - .get(&citekey) - .unwrap() - .keywords() - .unwrap() - .format_verbatim(); - keywords - } else { - let keywords = String::from(""); - keywords - } - }; - keywords - } - - pub fn get_abstract(citekey: &str, biblio: &Bibliography) -> String { - let text = { - if biblio.get(&citekey).unwrap().abstract_().is_ok() { - let abstract_text = biblio - .get(&citekey) - .unwrap() - .abstract_() - .unwrap() - .format_verbatim(); - abstract_text - } else { - let abstract_text = format!("No abstract"); - abstract_text - } - }; - text - } - - pub fn get_weblink(citekey: &str, biblio: &Bibliography) -> String { - if let true = biblio.get(&citekey).unwrap().doi().is_ok() { - let url = biblio.get(&citekey).unwrap().doi().unwrap(); - url - } else if let true = biblio.get(&citekey).unwrap().url().is_ok() { - let url = biblio.get(&citekey).unwrap().url().unwrap(); - url - } else { - let url = "".to_string(); - url - } - } - - pub fn get_filepath(citekey: &str, biblio: &Bibliography) -> String { - if let true = biblio.get(&citekey).unwrap().file().is_ok() { - let file = biblio.get(&citekey).unwrap().file().unwrap(); - file - } else { - let file = "".to_string(); - file - } - } -} diff --git a/src/bib/entries.rs b/src/bib/entries.rs deleted file mode 100644 index 41edba8..0000000 --- a/src/bib/entries.rs +++ /dev/null @@ -1,258 +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::bib::bibmain::BibiData; -use ratatui::widgets::{ScrollbarState, TableState}; - -#[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 - } -} - -#[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/bib/keywords.rs b/src/bib/keywords.rs deleted file mode 100644 index 2668323..0000000 --- a/src/bib/keywords.rs +++ /dev/null @@ -1,55 +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 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(), - } - } -} diff --git a/src/bib/search.rs b/src/bib/search.rs deleted file mode 100644 index f6e8d14..0000000 --- a/src/bib/search.rs +++ /dev/null @@ -1,136 +0,0 @@ -use super::entries::EntryTableItem; -use nucleo_matcher::{ - pattern::{CaseMatching, Normalization, Pattern}, - Config, Matcher, -}; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct BibiSearch { - pub search_string: String, // Search string show in footer, used for search - pub inner_search: bool, // True, if we trigger a search for already filtered list - pub filtered_tag_list: Vec, -} - -impl Default for BibiSearch { - fn default() -> Self { - Self { - search_string: String::new(), - inner_search: false, - filtered_tag_list: Vec::new(), - } - } -} - -impl BibiSearch { - // Stringify EntryTableItem by joining/concat - fn convert_to_string(inner_vec: &EntryTableItem) -> String { - let entry_table_item_str = { - format!( - "{} {} {} {} {} {}", - &inner_vec.authors, - &inner_vec.title, - &inner_vec.year, - &inner_vec.pubtype, - &inner_vec.keywords, - &inner_vec.citekey - ) - }; - entry_table_item_str - } - - // Return a filtered entry list - pub fn search_entry_list( - search_pattern: &str, - orig_list: Vec, - ) -> Vec { - // Create a hashmap to connect stingified entry with entry vec - let mut entry_string_hm: HashMap = HashMap::new(); - - // Convert all entries to string and insert them into the hashmap - // next to the original inner Vec of the entry list - for entry in orig_list { - entry_string_hm.insert(Self::convert_to_string(&entry), entry); - } - - // Set up matcher (TODO: One time needed only, move to higher level) - let mut matcher = Matcher::new(Config::DEFAULT); - - // Filter the stringified entries and collect them into a vec - let filtered_matches: Vec = { - let matches = - Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) - .match_list(entry_string_hm.keys(), &mut matcher); - matches.into_iter().map(|f| f.0.to_string()).collect() - }; - - // Create filtered entry list and push the inner entry vec's to it - // Use the filtered stringified hm-key as index - let mut filtered_list: Vec = Vec::new(); - for m in filtered_matches { - filtered_list.push(entry_string_hm[&m].to_owned()); - } - filtered_list.sort(); - filtered_list - } - - pub fn search_tag_list(search_pattern: &str, orig_list: Vec) -> Vec { - // Set up matcher (TODO: One time needed only) - let mut matcher = Matcher::new(Config::DEFAULT); - - // Filter the list items by search pattern - let filtered_matches: Vec = { - let matches = - Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) - .match_list(orig_list, &mut matcher); - matches.into_iter().map(|f| f.0.to_string()).collect() - }; - filtered_matches - } - - pub fn filter_entries_by_tag( - keyword: &str, - orig_list: &Vec, - ) -> Vec { - let mut filtered_list: Vec = Vec::new(); - - // Loop over the whole given entry table - // Check if the selected keyword is present in the current entry - // If present, push the entry to the filtered list - for e in orig_list { - if e.keywords.contains(keyword) { - filtered_list.push(e.to_owned()); - } - } - - filtered_list - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vector_join() { - let bibvec: EntryTableItem = EntryTableItem { - authors: "Author".to_string(), - short_author: "".to_string(), - title: "Title".to_string(), - year: "1999".to_string(), - pubtype: "article".to_string(), - keywords: "hello, bye".to_string(), - citekey: "author_1999".to_string(), - abstract_text: "An abstract with multiple sentences. Here is the second".to_string(), - doi_url: "https://www.bibiman.org".to_string(), - filepath: "/home/file/path.pdf".to_string(), - }; - - let joined_vec = BibiSearch::convert_to_string(&bibvec); - - assert_eq!( - joined_vec, - "Author Title 1999 article hello, bye author_1999" - ) - } -} diff --git a/src/bibiman.rs b/src/bibiman.rs new file mode 100644 index 0000000..3bb731b --- /dev/null +++ b/src/bibiman.rs @@ -0,0 +1,223 @@ +// 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::bibiman::{bibisetup::*, search::BibiSearch}; +use crate::cliargs::CLIArgs; +use crate::{bibiman::entries::EntryTable, bibiman::keywords::TagList}; +use arboard::Clipboard; +use color_eyre::eyre::{Ok, Result}; +use std::path::PathBuf; + +pub mod bibisetup; +pub mod entries; +pub mod keywords; +pub mod search; + +// 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 Bibiman { + // main bib file + pub main_bibfile: PathBuf, + // main bibliography + pub main_biblio: BibiSetup, + // 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 Bibiman { + // Constructs a new instance of [`App`]. + pub fn new(args: CLIArgs) -> Result { + let main_bibfile = args.bibfilearg; + let main_biblio = BibiSetup::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 { + main_bibfile, + main_biblio, + tag_list, + search_struct, + entry_table, + scroll_info: 0, + current_area, + former_area: None, + }) + } + + pub fn update_lists(&mut self) { + self.main_biblio = BibiSetup::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/bibiman/bibisetup.rs b/src/bibiman/bibisetup.rs new file mode 100644 index 0000000..abc91e4 --- /dev/null +++ b/src/bibiman/bibisetup.rs @@ -0,0 +1,279 @@ +// 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 biblatex::{self, Bibliography}; +use biblatex::{ChunksExt, Type}; +use itertools::Itertools; +use std::{fs, path::PathBuf}; + +#[derive(Debug)] +pub enum FileFormat { + BibLatex, + Hayagriva, +} + +// Set necessary fields +// TODO: can surely be made more efficient/simpler +#[derive(Debug)] +pub struct BibiSetup { + pub bibfile: PathBuf, // path to bibfile + pub bibfile_format: FileFormat, // Format of passed file + pub bibfilestring: String, // content of bibfile as string + pub bibliography: Bibliography, // parsed bibliography + pub citekeys: Vec, // list of all citekeys + pub keyword_list: Vec, // list of all available keywords + pub entry_list: Vec, // List of all entries +} + +#[derive(Debug, Clone)] +pub struct BibiData { + pub authors: 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 BibiSetup { + pub fn new(main_bibfile: PathBuf) -> Self { + // TODO: Needs check for config file path as soon as config file is impl + let bibfile_format = Self::check_file_format(&main_bibfile); + let bibfile = main_bibfile; + let bibfilestring = fs::read_to_string(&bibfile).unwrap(); + let bibliography = biblatex::Bibliography::parse(&bibfilestring).unwrap(); + let citekeys = Self::get_citekeys(&bibliography); + let keyword_list = Self::collect_tag_list(&citekeys, &bibliography); + let entry_list = Self::create_entry_list(&citekeys, &bibliography); + Self { + bibfile, + bibfile_format, + bibfilestring, + bibliography, + citekeys, + keyword_list, + entry_list, + } + } + + // Check which file format the passed file has + fn check_file_format(main_bibfile: &PathBuf) -> FileFormat { + let extension = main_bibfile.extension().unwrap().to_str(); + + match extension { + Some("yml") => FileFormat::Hayagriva, + Some("yaml") => FileFormat::Hayagriva, + Some("bib") => FileFormat::BibLatex, + Some(_) => panic!("The extension {:?} is no valid bibfile", extension.unwrap()), + None => panic!("The given path {:?} holds no valid file", main_bibfile), + } + } + + fn create_entry_list(citekeys: &[String], bibliography: &Bibliography) -> Vec { + citekeys + .into_iter() + .map(|k| BibiData { + authors: Self::get_authors(&k, &bibliography), + title: Self::get_title(&k, &bibliography), + year: Self::get_year(&k, &bibliography), + pubtype: Self::get_pubtype(&k, &bibliography), + keywords: Self::get_keywords(&k, &bibliography), + citekey: k.to_owned(), + abstract_text: Self::get_abstract(&k, &bibliography), + doi_url: Self::get_weblink(&k, &bibliography), + filepath: Self::get_filepath(&k, &bibliography), + }) + .collect() + } + + // get list of citekeys from the given bibfile + // this list is the base for further operations on the bibentries + // since it is the entry point of the biblatex crate. + pub fn get_citekeys(bibstring: &Bibliography) -> Vec { + let citekeys: Vec = bibstring.keys().map(|k| k.to_owned()).collect(); + citekeys + } + + // collect all keywords present in the bibliography + // sort them and remove duplicates + // this list is for fast filtering entries by topics/keyowrds + pub fn collect_tag_list(citekeys: &[String], biblio: &Bibliography) -> Vec { + // Initialize vector collecting all keywords + let mut keyword_list = vec![]; + + // Loop over entries and collect all keywords + for i in citekeys { + if biblio.get(&i).unwrap().keywords().is_ok() { + let items = biblio + .get(&i) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + // Split keyword string into slices, trim leading and trailing + // whitespaces, remove empty slices, and collect them + let mut key_vec: Vec = items + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + // Append keywords to vector + keyword_list.append(&mut key_vec); + } + } + + // Sort the vector and remove duplicates + keyword_list.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + keyword_list.dedup(); + keyword_list + } + + pub fn get_authors(citekey: &str, biblio: &Bibliography) -> String { + if biblio.get(&citekey).unwrap().author().is_ok() { + let authors = biblio.get(&citekey).unwrap().author().unwrap(); + if authors.len() > 1 { + let all_authors = authors.iter().map(|a| &a.name).join(", "); + all_authors + } else if authors.len() == 1 { + let authors = authors[0].name.to_string(); + authors + } else { + let editors_authors = format!("empty"); + editors_authors + } + } else { + if !biblio.get(&citekey).unwrap().editors().unwrap().is_empty() { + let editors = biblio.get(&citekey).unwrap().editors().unwrap(); + if editors[0].0.len() > 1 { + // let editors = format!("{} (ed.) et al.", editors[0].0[0].name); + let mut editors = editors[0].0.iter().map(|e| &e.name).join(", "); + editors.push_str(" (ed.)"); + editors + } else if editors[0].0.len() == 1 { + let editors = format!("{} (ed.)", editors[0].0[0].name); + editors + } else { + let editors_authors = format!("empty"); + editors_authors + } + } else { + let editors_authors = format!("empty"); + editors_authors + } + } + } + + pub fn get_title(citekey: &str, biblio: &Bibliography) -> String { + let title = { + if biblio.get(&citekey).unwrap().title().is_ok() { + let title = biblio + .get(&citekey) + .unwrap() + .title() + .unwrap() + .format_verbatim(); + title + } else { + let title = format!("no title"); + title + } + }; + title + } + + pub fn get_year(citekey: &str, biblio: &Bibliography) -> String { + let year = biblio.get(&citekey).unwrap(); + let year = { + if year.date().is_ok() { + let year = year.date().unwrap().to_chunks().format_verbatim(); + let year = year[..4].to_string(); + year + } else { + let year = format!("n.d."); + year + } + }; + year + } + + pub fn get_pubtype(citekey: &str, biblio: &Bibliography) -> String { + let pubtype = biblio.get(&citekey).unwrap().entry_type.to_string(); + pubtype + } + + pub fn get_keywords(citekey: &str, biblio: &Bibliography) -> String { + let keywords = { + if biblio.get(&citekey).unwrap().keywords().is_ok() { + let keywords = biblio + .get(&citekey) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + keywords + } else { + let keywords = String::from(""); + keywords + } + }; + keywords + } + + pub fn get_abstract(citekey: &str, biblio: &Bibliography) -> String { + let text = { + if biblio.get(&citekey).unwrap().abstract_().is_ok() { + let abstract_text = biblio + .get(&citekey) + .unwrap() + .abstract_() + .unwrap() + .format_verbatim(); + abstract_text + } else { + let abstract_text = format!("No abstract"); + abstract_text + } + }; + text + } + + pub fn get_weblink(citekey: &str, biblio: &Bibliography) -> String { + if let true = biblio.get(&citekey).unwrap().doi().is_ok() { + let url = biblio.get(&citekey).unwrap().doi().unwrap(); + url + } else if let true = biblio.get(&citekey).unwrap().url().is_ok() { + let url = biblio.get(&citekey).unwrap().url().unwrap(); + url + } else { + let url = "".to_string(); + url + } + } + + pub fn get_filepath(citekey: &str, biblio: &Bibliography) -> String { + if let true = biblio.get(&citekey).unwrap().file().is_ok() { + let file = biblio.get(&citekey).unwrap().file().unwrap(); + file + } else { + let file = "".to_string(); + file + } + } +} diff --git a/src/bibiman/entries.rs b/src/bibiman/entries.rs new file mode 100644 index 0000000..2c222d1 --- /dev/null +++ b/src/bibiman/entries.rs @@ -0,0 +1,258 @@ +// 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::bibiman::bibisetup::BibiData; +use ratatui::widgets::{ScrollbarState, TableState}; + +#[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 + } +} + +#[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/bibiman/keywords.rs b/src/bibiman/keywords.rs new file mode 100644 index 0000000..2668323 --- /dev/null +++ b/src/bibiman/keywords.rs @@ -0,0 +1,55 @@ +// 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 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(), + } + } +} diff --git a/src/bibiman/search.rs b/src/bibiman/search.rs new file mode 100644 index 0000000..f6e8d14 --- /dev/null +++ b/src/bibiman/search.rs @@ -0,0 +1,136 @@ +use super::entries::EntryTableItem; +use nucleo_matcher::{ + pattern::{CaseMatching, Normalization, Pattern}, + Config, Matcher, +}; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct BibiSearch { + pub search_string: String, // Search string show in footer, used for search + pub inner_search: bool, // True, if we trigger a search for already filtered list + pub filtered_tag_list: Vec, +} + +impl Default for BibiSearch { + fn default() -> Self { + Self { + search_string: String::new(), + inner_search: false, + filtered_tag_list: Vec::new(), + } + } +} + +impl BibiSearch { + // Stringify EntryTableItem by joining/concat + fn convert_to_string(inner_vec: &EntryTableItem) -> String { + let entry_table_item_str = { + format!( + "{} {} {} {} {} {}", + &inner_vec.authors, + &inner_vec.title, + &inner_vec.year, + &inner_vec.pubtype, + &inner_vec.keywords, + &inner_vec.citekey + ) + }; + entry_table_item_str + } + + // Return a filtered entry list + pub fn search_entry_list( + search_pattern: &str, + orig_list: Vec, + ) -> Vec { + // Create a hashmap to connect stingified entry with entry vec + let mut entry_string_hm: HashMap = HashMap::new(); + + // Convert all entries to string and insert them into the hashmap + // next to the original inner Vec of the entry list + for entry in orig_list { + entry_string_hm.insert(Self::convert_to_string(&entry), entry); + } + + // Set up matcher (TODO: One time needed only, move to higher level) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the stringified entries and collect them into a vec + let filtered_matches: Vec = { + let matches = + Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(entry_string_hm.keys(), &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + + // Create filtered entry list and push the inner entry vec's to it + // Use the filtered stringified hm-key as index + let mut filtered_list: Vec = Vec::new(); + for m in filtered_matches { + filtered_list.push(entry_string_hm[&m].to_owned()); + } + filtered_list.sort(); + filtered_list + } + + pub fn search_tag_list(search_pattern: &str, orig_list: Vec) -> Vec { + // Set up matcher (TODO: One time needed only) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the list items by search pattern + let filtered_matches: Vec = { + let matches = + Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(orig_list, &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + filtered_matches + } + + pub fn filter_entries_by_tag( + keyword: &str, + orig_list: &Vec, + ) -> Vec { + let mut filtered_list: Vec = Vec::new(); + + // Loop over the whole given entry table + // Check if the selected keyword is present in the current entry + // If present, push the entry to the filtered list + for e in orig_list { + if e.keywords.contains(keyword) { + filtered_list.push(e.to_owned()); + } + } + + filtered_list + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector_join() { + let bibvec: EntryTableItem = EntryTableItem { + authors: "Author".to_string(), + short_author: "".to_string(), + title: "Title".to_string(), + year: "1999".to_string(), + pubtype: "article".to_string(), + keywords: "hello, bye".to_string(), + citekey: "author_1999".to_string(), + abstract_text: "An abstract with multiple sentences. Here is the second".to_string(), + doi_url: "https://www.bibiman.org".to_string(), + filepath: "/home/file/path.pdf".to_string(), + }; + + let joined_vec = BibiSearch::convert_to_string(&bibvec); + + assert_eq!( + joined_vec, + "Author Title 1999 article hello, bye author_1999" + ) + } +} diff --git a/src/main.rs b/src/main.rs index eaa9e05..c2c5121 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,12 +15,13 @@ // along with this program. If not, see . ///// +use app::App; use cliargs::CLIArgs; use color_eyre::eyre::Result; use errorsetup::init_error_hooks; -use tui::app::App; -pub mod bib; +pub mod app; +pub mod bibiman; pub mod cliargs; pub mod errorsetup; pub mod tui; diff --git a/src/tui.rs b/src/tui.rs index 83d0b13..7b8b81f 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -15,12 +15,12 @@ // along with this program. If not, see . ///// -pub mod app; pub mod command; +pub mod commandnew; pub mod handler; pub mod ui; -use crate::tui::app::App; +use crate::App; use crossterm::{ cursor, event::{ @@ -59,9 +59,9 @@ pub struct Tui { /// Interface to the Terminal. pub terminal: ratatui::Terminal>, /// Event sender channel. - sender: mpsc::UnboundedSender, + evt_sender: mpsc::UnboundedSender, /// Event receiver channel. - receiver: mpsc::UnboundedReceiver, + evt_receiver: mpsc::UnboundedReceiver, /// Event handler thread. handler: tokio::task::JoinHandle<()>, cancellation_token: CancellationToken, @@ -71,13 +71,13 @@ impl Tui { // Constructs a new instance of [`Tui`]. pub fn new() -> Result { let terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; - let (sender, receiver) = mpsc::unbounded_channel(); + let (evt_sender, evt_receiver) = mpsc::unbounded_channel(); let handler = tokio::spawn(async {}); let cancellation_token = CancellationToken::new(); Ok(Self { terminal, - sender, - receiver, + evt_sender, + evt_receiver, handler, cancellation_token, }) @@ -88,7 +88,7 @@ impl Tui { self.cancel(); self.cancellation_token = CancellationToken::new(); let event_loop = Self::event_loop( - self.sender.clone(), + self.evt_sender.clone(), self.cancellation_token.clone(), tick_rate, ); @@ -203,7 +203,10 @@ impl Tui { } pub async fn next(&mut self) -> Result { - self.receiver.recv().await.ok_or_eyre("This is an IO error") + self.evt_receiver + .recv() + .await + .ok_or_eyre("This is an IO error") } } diff --git a/src/tui/app.rs b/src/tui/app.rs deleted file mode 100644 index b09ae80..0000000 --- a/src/tui/app.rs +++ /dev/null @@ -1,257 +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::Event; -use crate::bib::{bibmain::*, search::BibiSearch}; -use crate::cliargs::CLIArgs; -use crate::tui; -use crate::{bib::entries::EntryTable, bib::keywords::TagList, tui::handler::handle_key_events}; -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/tui/command.rs b/src/tui/command.rs index 9f25f5f..8416a3e 100644 --- a/src/tui/command.rs +++ b/src/tui/command.rs @@ -15,9 +15,9 @@ // along with this program. If not, see . ///// -use crate::bib::entries::EntryTableColumn; -use crate::bib::search::BibiSearch; -use crate::tui::app::{App, FormerArea}; +use crate::bibiman::entries::EntryTableColumn; +use crate::bibiman::search::BibiSearch; +use crate::bibiman::{Bibiman, FormerArea}; use crate::tui::Tui; use color_eyre::eyre::{Context, Ok, Result}; use core::panic; @@ -25,7 +25,7 @@ use editor_command::EditorBuilder; use ratatui::widgets::ScrollbarState; use std::process::{Command, Stdio}; -impl App { +impl Bibiman { // Entry Table commands // Movement @@ -260,7 +260,7 @@ impl App { } } -impl App { +impl Bibiman { // Tag List commands // Movement diff --git a/src/tui/commandnew.rs b/src/tui/commandnew.rs new file mode 100644 index 0000000..45b2f52 --- /dev/null +++ b/src/tui/commandnew.rs @@ -0,0 +1,178 @@ +// 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 ratatui::crossterm::event::{ + Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, +}; +use tui_input::Input; + +// Possible scroll areas. +#[derive(Debug, PartialEq, Eq)] +pub enum ScrollType { + Rows, + Cols, + InfoArea, +} + +// Possible ressources to open +#[derive(Debug, PartialEq, Eq)] +pub enum OpenRessource { + PDF, + WebLink, + Note, +} + +/// Application command. +#[derive(Debug, PartialEq, Eq)] +pub enum Command { + // Toggle area + ToggleArea, + // Next + Next(ScrollType, usize), + // Previous. + Previous(ScrollType, usize), + // Go to top. + Top, + // Go to bottom. + Bottom, + // Search list + SearchList, + // Reset lists + ResetList, + // Confirm search/selection + Confirm, + // Sort table/list + SortList, + // Yank selected item + YankItem, + // Edit file + EditFile, + // Open linked ressource + Open(OpenRessource), + // Input command. + Input(InputCommand), + // Hexdump command. + Exit, + // Do nothing. + Nothing, +} + +impl From for Command { + fn from(key_event: KeyEvent) -> Self { + match key_event.code { + // Go to first/last entry of selected list/table + KeyCode::Char('g') | KeyCode::Home => Self::Top, + KeyCode::Char('G') | KeyCode::End => Self::Bottom, + // Scroll columns of EntryTable + KeyCode::Right | KeyCode::Char('l') => Self::Next(ScrollType::Cols, 1), + KeyCode::Left | KeyCode::Char('h') => Self::Previous(ScrollType::Cols, 1), + // Scroll table/list vertically by 1 + KeyCode::Down | KeyCode::Char('j') => Self::Next(ScrollType::Rows, 1), + KeyCode::Up | KeyCode::Char('k') => Self::Previous(ScrollType::Rows, 1), + // Scroll table/list vertically by 5 + KeyCode::PageDown => Self::Next(ScrollType::Rows, 5), + KeyCode::PageUp => Self::Previous(ScrollType::Rows, 5), + KeyCode::Char('d') => { + if key_event.modifiers == KeyModifiers::CONTROL { + Self::Next(ScrollType::Rows, 5) + } else { + Self::Nothing + } + } + KeyCode::Char('u') => { + if key_event.modifiers == KeyModifiers::CONTROL { + Self::Previous(ScrollType::Rows, 5) + } else { + Self::Open(OpenRessource::WebLink) + } + } + // Exit App + KeyCode::Char('q') => Self::Exit, + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + Self::Exit + } else { + Self::Nothing + } + } + // Switch selected area + KeyCode::Tab => Self::ToggleArea, + KeyCode::BackTab => Self::ToggleArea, + // Enter search mode + KeyCode::Char('/') => Self::Input(InputCommand::Enter), + KeyCode::Char('f') => { + if key_event.modifiers == KeyModifiers::CONTROL { + Self::Input(InputCommand::Enter) + } else { + Self::Nothing + } + } + // KeyCode::Backspace => Self::Input(InputCommand::Resume(Event::Key(key_event))), + // Confirm selection + KeyCode::Enter => Self::Confirm, + // Reset lists/tables + KeyCode::Esc => Self::ResetList, + // Open linked ressource + KeyCode::Char('o') => Self::Open(OpenRessource::PDF), + // KeyCode::Char('u') => Self::Open(OpenRessource::WebLink), + // Edit currently selected entry + KeyCode::Char('e') => Self::EditFile, + // Yank selected item/value + KeyCode::Char('y') => Self::YankItem, + // Else do nothing + _ => Self::Nothing, + } + } +} + +impl From for Command { + fn from(mouse_event: MouseEvent) -> Self { + match mouse_event.kind { + MouseEventKind::ScrollDown => Self::Next(ScrollType::Rows, 1), + MouseEventKind::ScrollUp => Self::Previous(ScrollType::Rows, 1), + _ => Self::Nothing, + } + } +} + +/// Input mode command. +#[derive(Debug, PartialEq, Eq)] +pub enum InputCommand { + // Handle input. + Handle(Event), + // Enter input mode. + Enter, + // Confirm input. + Confirm, + // Exit input mode + Exit, +} + +impl InputCommand { + /// Parses the event. + pub fn parse(key_event: KeyEvent, input: &Input) -> Self { + if key_event.code == KeyCode::Esc + || (key_event.code == KeyCode::Backspace && input.value().is_empty()) + { + Self::Exit + } else if key_event.code == KeyCode::Enter { + Self::Confirm + } else { + Self::Handle(Event::Key(key_event)) + } + } +} diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 5a196b5..3a4d055 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -15,12 +15,11 @@ // along with this program. If not, see . ///// -use crate::tui::app::App; +use crate::bibiman::{Bibiman, CurrentArea}; use crate::tui::Tui; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -use super::app::CurrentArea; +use crate::App; use color_eyre::eyre::Result; +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, tui: &mut Tui) -> Result<()> { @@ -37,159 +36,159 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R } } KeyCode::PageDown => { - app.scroll_info_down(); + app.bibiman.scroll_info_down(); } KeyCode::PageUp => { - app.scroll_info_up(); + app.bibiman.scroll_info_up(); } _ => {} } // Keycodes for specific areas - match app.current_area { + match app.bibiman.current_area { // Keycodes for the tag area CurrentArea::TagArea => match key_event.code { KeyCode::Down => { - app.select_next_tag(1); + app.bibiman.select_next_tag(1); } KeyCode::Up => { - app.select_previous_tag(1); + app.bibiman.select_previous_tag(1); } KeyCode::Char('j') => { if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_down(); + app.bibiman.scroll_info_down(); } else { - app.select_next_tag(1); + app.bibiman.select_next_tag(1); } } KeyCode::Char('k') => { if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_up(); + app.bibiman.scroll_info_up(); } else { - app.select_previous_tag(1); + app.bibiman.select_previous_tag(1); } } KeyCode::Char('d') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.select_next_tag(5) + app.bibiman.select_next_tag(5) } } KeyCode::Char('u') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.select_previous_tag(5) + app.bibiman.select_previous_tag(5) } } KeyCode::Char('g') | KeyCode::Home => { - app.select_first_tag(); + app.bibiman.select_first_tag(); } KeyCode::Char('G') | KeyCode::End => { - app.select_last_tag(); + app.bibiman.select_last_tag(); } KeyCode::Char('/') => { - app.enter_search_area(); + app.bibiman.enter_search_area(); } KeyCode::Char('f') | KeyCode::Char('F') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.enter_search_area(); + app.bibiman.enter_search_area(); } } KeyCode::Tab | KeyCode::BackTab => { - app.toggle_area(); + app.bibiman.toggle_area(); } KeyCode::Esc => { - app.reset_current_list(); + app.bibiman.reset_current_list(); } KeyCode::Enter => { - app.filter_for_tags(); + app.bibiman.filter_for_tags(); } _ => {} }, // Keycodes for the entry area CurrentArea::EntryArea => match key_event.code { KeyCode::Down => { - app.select_next_entry(1); + app.bibiman.select_next_entry(1); } KeyCode::Up => { - app.select_previous_entry(1); + app.bibiman.select_previous_entry(1); } KeyCode::Char('j') => { if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_down(); + app.bibiman.scroll_info_down(); } else { - app.select_next_entry(1); + app.bibiman.select_next_entry(1); } } KeyCode::Char('k') => { if key_event.modifiers == KeyModifiers::ALT { - app.scroll_info_up(); + app.bibiman.scroll_info_up(); } else { - app.select_previous_entry(1); + app.bibiman.select_previous_entry(1); } } KeyCode::Char('d') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.select_next_entry(5); + app.bibiman.select_next_entry(5); } } KeyCode::Char('u') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.select_previous_entry(5); + app.bibiman.select_previous_entry(5); } else { - app.open_doi_url()?; + app.bibiman.open_doi_url()?; } } KeyCode::Char('g') | KeyCode::Home => { - app.select_first_entry(); + app.bibiman.select_first_entry(); } KeyCode::Char('G') | KeyCode::End => { - app.select_last_entry(); + app.bibiman.select_last_entry(); } KeyCode::Char('h') => { - app.select_prev_column(); + app.bibiman.select_prev_column(); } KeyCode::Char('l') => { - app.select_next_column(); + app.bibiman.select_next_column(); } KeyCode::Char('s') => { - app.entry_table.sort_entry_table(true); + app.bibiman.entry_table.sort_entry_table(true); } KeyCode::Char('y') => { - App::yank_text(&app.get_selected_citekey()); + Bibiman::yank_text(&app.bibiman.get_selected_citekey()); } KeyCode::Char('e') => { - app.run_editor(tui)?; + app.bibiman.run_editor(tui)?; } KeyCode::Char('o') => { - app.open_connected_file()?; + app.bibiman.open_connected_file()?; } KeyCode::Char('/') => { - app.enter_search_area(); + app.bibiman.enter_search_area(); } KeyCode::Char('f') | KeyCode::Char('F') => { if key_event.modifiers == KeyModifiers::CONTROL { - app.enter_search_area(); + app.bibiman.enter_search_area(); } } KeyCode::Tab | KeyCode::BackTab => { - app.toggle_area(); + app.bibiman.toggle_area(); } KeyCode::Esc => { - app.reset_current_list(); + app.bibiman.reset_current_list(); } _ => {} }, // Keycodes for the search area (rendered in footer) CurrentArea::SearchArea => match key_event.code { KeyCode::Esc => { - app.break_search(); + app.bibiman.break_search(); } KeyCode::Enter => { - app.confirm_search(); + app.bibiman.confirm_search(); } KeyCode::Backspace => { - app.search_pattern_pop(); + app.bibiman.search_pattern_pop(); } KeyCode::Char(search_pattern) => { - app.search_pattern_push(search_pattern); + app.bibiman.search_pattern_push(search_pattern); } _ => {} }, @@ -199,8 +198,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R app.quit(); } KeyCode::Esc => { - app.toggle_area(); - app.former_area = None; + app.bibiman.toggle_area(); + app.bibiman.former_area = None; } _ => {} }, diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 07bc88d..d5571c8 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -15,10 +15,10 @@ // along with this program. If not, see . ///// -use super::app::{CurrentArea, FormerArea}; -use crate::bib::entries::EntryTableColumn; -use crate::bib::keywords::TagListItem; -use crate::tui::app::App; +use crate::bibiman::entries::EntryTableColumn; +use crate::bibiman::keywords::TagListItem; +use crate::bibiman::{Bibiman, CurrentArea, FormerArea}; +use crate::App; use ratatui::{ buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, @@ -87,18 +87,18 @@ impl Widget for &mut App { 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); + Bibiman::render_header(header_area, buf); + self.bibiman.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); + self.bibiman.render_entrytable(entry_area, buf); + self.bibiman.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); + self.bibiman.render_taglist(tag_area, buf); + self.bibiman.render_selected_item(info_area, buf); } } -impl App { +impl Bibiman { pub fn render_header(area: Rect, buf: &mut Buffer) { Paragraph::new("BIBIMAN – BibLaTeX manager TUI") .bold() -- cgit v1.2.3