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