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