aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/backend.rs2
-rw-r--r--src/backend/bib.rs51
-rw-r--r--src/backend/cliargs.rs89
-rw-r--r--src/frontend.rs5
-rw-r--r--src/frontend/app.rs221
-rw-r--r--src/frontend/event.rs97
-rw-r--r--src/frontend/handler.rs70
-rw-r--r--src/frontend/tui.rs77
-rw-r--r--src/frontend/ui.rs201
-rw-r--r--src/main.rs67
10 files changed, 880 insertions, 0 deletions
diff --git a/src/backend.rs b/src/backend.rs
new file mode 100644
index 0000000..7907145
--- /dev/null
+++ b/src/backend.rs
@@ -0,0 +1,2 @@
+pub mod bib;
+pub mod cliargs;
diff --git a/src/backend/bib.rs b/src/backend/bib.rs
new file mode 100644
index 0000000..aa7272f
--- /dev/null
+++ b/src/backend/bib.rs
@@ -0,0 +1,51 @@
+use biblatex::Bibliography;
+use regex::Regex;
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+
+use super::cliargs::{CLIArgs, PosArgs};
+
+// Set necessary fields
+// TODO: can surely be made more efficient/simpler
+pub struct Bibi {
+ pub citekeys: Vec<String>,
+ // pub bibliography: Bibliography,
+}
+
+pub fn get_bibfile(filename: impl AsRef<Path>) -> String {
+ let bibfile = fs::read_to_string(&filename).unwrap();
+ bibfile
+}
+
+pub fn get_citekeys(bibstring: &Bibliography) -> Vec<String> {
+ // let bib = Bibliography::parse(&get_bibfile(CLIArgs::parse_cli_args().bibfilearg)).unwrap();
+ // // Define Regex to match citekeys
+ // let re = Regex::new(r"(?m)^\@[a-zA-Z]*\{(.*)\,").unwrap();
+ // // Declare empty vector to fill with captured keys
+ // // Has to be Vec<&str> because of captures_iter method
+ // let mut keys = vec![];
+ // for (_, [key]) in re.captures_iter(&bibfilestring).map(|c| c.extract()) {
+ // keys.push(key);
+ // }
+ // // Transform Vec<&str> to Vec<String> which is needed by the struct Bibi
+ // let mut citekeys: Vec<String> = keys.into_iter().map(String::from).collect();
+ // // Sort vector items case-insensitive
+ // citekeys.sort_by_key(|name| name.to_lowercase());
+ // citekeys
+ let mut citekeys: Vec<String> = bibstring.iter().map(|entry| entry.to_owned().key).collect();
+ citekeys.sort_by_key(|name| name.to_lowercase());
+ citekeys
+}
+
+impl Bibi {
+ pub fn new() -> Self {
+ // TODO: Needs check for config file path as soon as config file is impl
+ let bib = Bibliography::parse(&get_bibfile(PosArgs::parse_pos_args().bibfilearg)).unwrap();
+ Self {
+ citekeys: get_citekeys(&bib),
+ // bibliography: biblatex::Bibliography::parse(&bibfilestring).unwrap(),
+ }
+ }
+}
diff --git a/src/backend/cliargs.rs b/src/backend/cliargs.rs
new file mode 100644
index 0000000..b820b6a
--- /dev/null
+++ b/src/backend/cliargs.rs
@@ -0,0 +1,89 @@
+use core::panic;
+use std::path::{Path, PathBuf};
+
+use sarge::prelude::*;
+
+sarge! {
+ // Name of the struct
+ ArgumentsCLI,
+
+ // Show help and exit.
+ 'h' help: bool,
+
+ // Show version and exit. TODO: Write version...
+ 'v' version: bool,
+
+ // Option for file: -b - short option; --bibfile - long option
+ // #ok makes it optional
+ #ok 'b' bibfile: String,
+}
+
+// struct for CLIArgs
+pub struct CLIArgs {
+ pub helparg: bool,
+ pub versionarg: bool,
+}
+
+impl CLIArgs {
+ pub fn parse_cli_args() -> Self {
+ let (cli_args, _) = ArgumentsCLI::parse().expect("Could not parse CLI arguments");
+ Self {
+ helparg: cli_args.help,
+ versionarg: cli_args.version,
+ }
+ }
+}
+
+// Struct for positional arguments
+// TODO: Can surely be improved!!
+pub struct PosArgs {
+ pub bibfilearg: PathBuf,
+}
+
+impl PosArgs {
+ pub fn parse_pos_args() -> Self {
+ let (_, pos_args) = ArgumentsCLI::parse().expect("Could not parse positional arguments");
+ Self {
+ bibfilearg: if pos_args.len() > 1 {
+ PathBuf::from(&pos_args[1])
+ // pos_args[1].to_string()
+ } else {
+ panic!("No path to bibfile provided as argument")
+ }, // bibfilearg: pos_args[1].to_string(),
+ }
+ }
+}
+
+pub fn help_func() -> String {
+ let help = format!(
+ "\
+{} {}
+
+USAGE:
+ bibiman [FLAGS] [file]
+
+POSITIONAL ARGS:
+ <file> 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
new file mode 100644
index 0000000..6ec15f2
--- /dev/null
+++ b/src/frontend.rs
@@ -0,0 +1,5 @@
+pub mod app;
+pub mod event;
+pub mod ui;
+pub mod tui;
+pub mod handler;
diff --git a/src/frontend/app.rs b/src/frontend/app.rs
new file mode 100644
index 0000000..ce250cd
--- /dev/null
+++ b/src/frontend/app.rs
@@ -0,0 +1,221 @@
+use crate::backend::bib::*;
+use std::error;
+
+use ratatui::widgets::ListState;
+
+// Application result type.
+pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
+
+// Areas in which actions are possible
+#[derive(Debug)]
+pub enum CurrentArea {
+ EntryArea,
+ TagArea,
+ // SearchArea,
+}
+
+// Application.
+#[derive(Debug)]
+pub struct App {
+ // Is the application running?
+ pub running: bool,
+ // list
+ pub tag_list: TagList,
+ // TODO: table items
+ pub entry_list: EntryList,
+ // area
+ pub current_area: CurrentArea,
+}
+
+// Define the fundamental List
+#[derive(Debug)]
+pub struct TagList {
+ pub tag_list_items: Vec<TagListItem>,
+ pub tag_list_state: ListState,
+}
+
+// Structure of the list items. Can be a simple string or something more elaborated
+// eg:
+// struct TagListItem {
+// todo: String,
+// info: String,
+// status: Status,
+// }
+// where Status has to be defined explicitly somewhere else
+#[derive(Debug)]
+pub struct TagListItem {
+ pub info: String,
+}
+
+// Function to process inputed characters and convert them (to string, or more complex function)
+impl TagListItem {
+ pub fn new(info: &str) -> Self {
+ Self {
+ info: info.to_string(),
+ }
+ }
+}
+
+// INFO: in the original template it was <&'static str> instead of <String>
+impl FromIterator<String> for TagList {
+ // INFO: Here to originally <&'static str>
+ fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
+ let tag_list_items = iter
+ .into_iter()
+ // INFO: here originally not borrowed (without Ampersand'&')
+ .map(|info| TagListItem::new(&info))
+ .collect();
+ let tag_list_state = ListState::default(); // for preselection: .with_selected(Some(0));
+ Self {
+ tag_list_items,
+ tag_list_state,
+ }
+ }
+}
+
+impl FromIterator<(String, String)> for EntryList {
+ fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
+ let entry_list_items = iter
+ .into_iter()
+ .map(|(authors, title)| EntryListItem::new(&authors, &title))
+ .collect();
+ let entry_list_state = ListState::default();
+ Self {
+ entry_list_items,
+ entry_list_state,
+ }
+ }
+}
+
+// Define list containing entries as table
+#[derive(Debug)]
+pub struct EntryList {
+ pub entry_list_items: Vec<EntryListItem>,
+ pub entry_list_state: ListState,
+}
+
+// Define contents of each entry table row
+#[derive(Debug)]
+pub struct EntryListItem {
+ pub authors: String,
+ pub title: String,
+ // pub year: u16,
+}
+
+impl EntryListItem {
+ pub fn new(authors: &str, title: &str) -> Self {
+ Self {
+ authors: authors.to_string(),
+ title: title.to_string(),
+ }
+ }
+}
+
+impl Default for App {
+ fn default() -> Self {
+ // TEST: read file
+ let lines = Bibi::new().citekeys;
+ let iter = vec![
+ (
+ "Mrs. Doubtfire".to_string(),
+ "A great book of great length".to_string(),
+ ),
+ ("Veye Tatah".to_string(), "Modern economy".to_string()),
+ ("Joseph Conrad".to_string(), "Heart of Darkness".to_string()),
+ (
+ "Michelle-Rolpg Trouillot".to_string(),
+ "Silencing the Past".to_string(),
+ ),
+ ("Zora Neale Hurston".to_string(), "Barracoon".to_string()),
+ ];
+ // let mylist = ["Item 1", "Item 2"];
+ Self {
+ running: true,
+ // INFO: here the function(s) for creating the list has to be placed inside the parantheses -> Bib::whatever
+ tag_list: TagList::from_iter(lines),
+ entry_list: EntryList::from_iter(iter),
+ current_area: CurrentArea::EntryArea,
+ }
+ }
+}
+
+impl App {
+ // Constructs a new instance of [`App`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ // Handles the tick event of the terminal.
+ pub fn tick(&self) {}
+
+ // Set running to false to quit the application.
+ pub fn quit(&mut self) {
+ self.running = false;
+ }
+
+ // Toggle moveable list between entries and tags
+ pub fn toggle_area(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.current_area = CurrentArea::TagArea,
+ CurrentArea::TagArea => self.current_area = CurrentArea::EntryArea,
+ }
+ }
+
+ pub fn select_none(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.entry_list.entry_list_state.select(None),
+ CurrentArea::TagArea => self.tag_list.tag_list_state.select(None),
+ }
+ // self.tag_list.tag_list_state.select(None);
+ }
+
+ pub fn select_next(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.entry_list.entry_list_state.select_next(),
+ CurrentArea::TagArea => self.tag_list.tag_list_state.select_next(),
+ }
+ // self.tag_list.tag_list_state.select_next();
+ }
+ pub fn select_previous(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.entry_list.entry_list_state.select_previous(),
+ CurrentArea::TagArea => self.tag_list.tag_list_state.select_previous(),
+ }
+ // self.tag_list.tag_list_state.select_previous();
+ }
+
+ pub fn select_first(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.entry_list.entry_list_state.select_first(),
+ CurrentArea::TagArea => self.tag_list.tag_list_state.select_first(),
+ }
+ // self.tag_list.tag_list_state.select_first();
+ }
+
+ pub fn select_last(&mut self) {
+ match self.current_area {
+ CurrentArea::EntryArea => self.entry_list.entry_list_state.select_last(),
+ CurrentArea::TagArea => self.tag_list.tag_list_state.select_last(),
+ }
+ // self.tag_list.tag_list_state.select_last();
+ }
+
+ // pub fn select_none(&mut self) {
+ // self.entry_list.entry_list_state.select(None);
+ // }
+
+ // pub fn select_next(&mut self) {
+ // self.entry_list.entry_list_state.select_next();
+ // }
+ // pub fn select_previous(&mut self) {
+ // self.entry_list.entry_list_state.select_previous();
+ // }
+
+ // pub fn select_first(&mut self) {
+ // self.entry_list.entry_list_state.select_first();
+ // }
+
+ // pub fn select_last(&mut self) {
+ // self.entry_list.entry_list_state.select_last();
+ // }
+}
diff --git a/src/frontend/event.rs b/src/frontend/event.rs
new file mode 100644
index 0000000..f83dfea
--- /dev/null
+++ b/src/frontend/event.rs
@@ -0,0 +1,97 @@
+use std::time::Duration;
+
+use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
+use futures::{FutureExt, StreamExt};
+use tokio::sync::mpsc;
+
+use crate::frontend::app::AppResult;
+
+/// 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),
+}
+
+/// Terminal event handler.
+#[allow(dead_code)]
+#[derive(Debug)]
+pub struct EventHandler {
+ /// Event sender channel.
+ sender: mpsc::UnboundedSender<Event>,
+ /// Event receiver channel.
+ receiver: mpsc::UnboundedReceiver<Event>,
+ /// Event handler thread.
+ handler: tokio::task::JoinHandle<()>,
+}
+
+impl EventHandler {
+ /// Constructs a new instance of [`EventHandler`].
+ pub fn new(tick_rate: u64) -> Self {
+ let tick_rate = Duration::from_millis(tick_rate);
+ let (sender, receiver) = mpsc::unbounded_channel();
+ let _sender = sender.clone();
+ let handler = tokio::spawn(async move {
+ 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;
+ }
+ _ = tick_delay => {
+ _sender.send(Event::Tick).unwrap();
+ }
+ 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(_) => {
+ },
+ }
+ }
+ };
+ }
+ });
+ Self {
+ sender,
+ receiver,
+ handler,
+ }
+ }
+
+ /// Receive the next event from the handler thread.
+ ///
+ /// This function will always block the current thread if
+ /// there is no data available and it's possible for more data to be sent.
+ pub async fn next(&mut self) -> AppResult<Event> {
+ self.receiver
+ .recv()
+ .await
+ .ok_or(Box::new(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "This is an IO error",
+ )))
+ }
+}
diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs
new file mode 100644
index 0000000..2cc8bb5
--- /dev/null
+++ b/src/frontend/handler.rs
@@ -0,0 +1,70 @@
+use crate::frontend::app::{App, AppResult};
+use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+
+use super::app::CurrentArea;
+
+/// Handles the key events and updates the state of [`App`].
+pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
+ // Keycodes activated for every area (high priority)
+ match key_event.code {
+ // Exit application on `ESC` or `q`
+ KeyCode::Esc | KeyCode::Char('q') => {
+ app.quit();
+ }
+ // Exit application on `Ctrl-C`
+ KeyCode::Char('c') | KeyCode::Char('C') => {
+ if key_event.modifiers == KeyModifiers::CONTROL {
+ app.quit();
+ }
+ }
+ _ => {}
+ }
+ // Keycodes for specific areas
+ match app.current_area {
+ // Keycodes for the tag area
+ CurrentArea::TagArea => match key_event.code {
+ KeyCode::Char('j') | KeyCode::Down => {
+ app.select_next();
+ }
+ KeyCode::Char('k') | KeyCode::Up => {
+ app.select_previous();
+ }
+ KeyCode::Char('h') | KeyCode::Left => {
+ app.select_none();
+ }
+ KeyCode::Char('g') | KeyCode::Home => {
+ app.select_first();
+ }
+ KeyCode::Char('G') | KeyCode::End => {
+ app.select_last();
+ }
+ KeyCode::Tab | KeyCode::BackTab => {
+ app.toggle_area();
+ }
+ _ => {}
+ },
+ // Keycodes for the entry area
+ CurrentArea::EntryArea => match key_event.code {
+ KeyCode::Char('j') | KeyCode::Down => {
+ app.select_next();
+ }
+ KeyCode::Char('k') | KeyCode::Up => {
+ app.select_previous();
+ }
+ KeyCode::Char('h') | KeyCode::Left => {
+ app.select_none();
+ }
+ KeyCode::Char('g') | KeyCode::Home => {
+ app.select_first();
+ }
+ KeyCode::Char('G') | KeyCode::End => {
+ app.select_last();
+ }
+ KeyCode::Tab | KeyCode::BackTab => {
+ app.toggle_area();
+ }
+ _ => {}
+ },
+ }
+ Ok(())
+}
diff --git a/src/frontend/tui.rs b/src/frontend/tui.rs
new file mode 100644
index 0000000..94db9ea
--- /dev/null
+++ b/src/frontend/tui.rs
@@ -0,0 +1,77 @@
+use crate::frontend::app::{App, AppResult};
+use crate::frontend::event::EventHandler;
+use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
+use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
+use ratatui::backend::Backend;
+use ratatui::Terminal;
+use std::io;
+use std::panic;
+
+/// Representation of a terminal user interface.
+///
+/// It is responsible for setting up the terminal,
+/// initializing the interface and handling the draw events.
+#[derive(Debug)]
+pub struct Tui<B: Backend> {
+ /// Interface to the Terminal.
+ terminal: Terminal<B>,
+ /// Terminal event handler.
+ pub events: EventHandler,
+}
+
+impl<B: Backend> Tui<B> {
+ /// Constructs a new instance of [`Tui`].
+ pub fn new(terminal: Terminal<B>, events: EventHandler) -> Self {
+ Self { terminal, events }
+ }
+
+ /// Initializes the terminal interface.
+ ///
+ /// It enables the raw mode and sets terminal properties.
+ pub fn init(&mut self) -> AppResult<()> {
+ terminal::enable_raw_mode()?;
+ crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
+
+ // Define a custom panic hook to reset the terminal properties.
+ // This way, you won't have your terminal messed up if an unexpected error happens.
+ let panic_hook = panic::take_hook();
+ panic::set_hook(Box::new(move |panic| {
+ Self::reset().expect("failed to reset the terminal");
+ panic_hook(panic);
+ }));
+
+ self.terminal.hide_cursor()?;
+ self.terminal.clear()?;
+ 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) -> AppResult<()> {
+ // self.terminal.draw(|frame| ui::render(app, frame))?;
+ self.terminal
+ .draw(|frame| frame.render_widget(app, frame.area()))?;
+ Ok(())
+ }
+
+ /// Resets the terminal interface.
+ ///
+ /// This function is also used for the panic hook to revert
+ /// the terminal properties if unexpected errors occur.
+ fn reset() -> AppResult<()> {
+ terminal::disable_raw_mode()?;
+ crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
+ Ok(())
+ }
+
+ /// Exits the terminal interface.
+ ///
+ /// It disables the raw mode and reverts back the terminal properties.
+ pub fn exit(&mut self) -> AppResult<()> {
+ Self::reset()?;
+ self.terminal.show_cursor()?;
+ Ok(())
+ }
+}
diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs
new file mode 100644
index 0000000..ec5e612
--- /dev/null
+++ b/src/frontend/ui.rs
@@ -0,0 +1,201 @@
+use ratatui::{
+ buffer::Buffer,
+ layout::{Constraint, Layout, Rect},
+ style::{
+ palette::tailwind::{GRAY, SLATE},
+ Color, Modifier, Style, Stylize,
+ },
+ symbols,
+ text::Line,
+ widgets::{
+ Block, HighlightSpacing, List, ListItem, Padding, Paragraph, StatefulWidget, Widget, Wrap,
+ },
+};
+
+use crate::frontend::app::{App, TagListItem};
+
+use super::app::EntryListItem;
+
+const MAIN_BLUE_COLOR: Color = Color::Indexed(39);
+const MAIN_PURPLE_COLOR: Color = Color::Indexed(129);
+const BOX_BORDER_STYLE_MAIN: Style = Style::new().fg(Color::White).bg(Color::Black);
+const NORMAL_ROW_BG: Color = Color::Black;
+const ALT_ROW_BG_COLOR: Color = Color::Indexed(234);
+const SELECTED_STYLE: Style = Style::new()
+ .fg(MAIN_BLUE_COLOR)
+ .add_modifier(Modifier::BOLD);
+const TEXT_FG_COLOR: Color = SLATE.c200;
+
+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.info), TEXT_FG_COLOR);
+ // match value.status {
+ // Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR),
+ // Status::Completed => {
+ // Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR)
+ // }
+ // };
+ ListItem::new(line)
+ }
+}
+
+impl From<&EntryListItem> for ListItem<'_> {
+ fn from(value: &EntryListItem) -> Self {
+ let line = Line::styled(format!("{}, {}", value.authors, value.title), 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(2),
+ Constraint::Fill(1),
+ Constraint::Length(1),
+ ])
+ .areas(area);
+
+ let [list_area, item_area] =
+ Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
+
+ let [tag_area, info_area] =
+ Layout::horizontal([Constraint::Percentage(30), Constraint::Fill(1)]).areas(item_area);
+
+ // Render header and footer
+ App::render_header(header_area, buf);
+ App::render_footer(footer_area, buf);
+ // Render list area where entry gets selected
+ self.render_entry_list(list_area, buf);
+ // Render infos related to selected entry
+ // TODO: only placeholder at the moment, has to be impl.
+ 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("Ratatui List Example")
+ .bold()
+ .centered()
+ .render(area, buf);
+ }
+
+ pub fn render_footer(area: Rect, buf: &mut Buffer) {
+ Paragraph::new("Use g/h to move, h to unselect, g/G to go top/bottom.")
+ .centered()
+ .render(area, buf);
+ }
+
+ pub fn render_entry_list(&mut self, area: Rect, buf: &mut Buffer) {
+ let block = Block::bordered()
+ .title(
+ Line::raw(" Selection List ")
+ .centered()
+ .fg(Color::Indexed(39)),
+ )
+ // .borders(Borders::TOP)
+ .border_set(symbols::border::ROUNDED)
+ .border_style(BOX_BORDER_STYLE_MAIN)
+ .bg(Color::Black); // .bg(NORMAL_ROW_BG);
+
+ // Iterate through all elements in the `items` and stylize them.
+ let items: Vec<ListItem> = self
+ .entry_list
+ .entry_list_items
+ .iter()
+ .enumerate()
+ .map(|(i, todo_item)| {
+ let color = alternate_colors(i);
+ ListItem::from(todo_item).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(
+ Style::new()
+ .fg(MAIN_PURPLE_COLOR)
+ .add_modifier(Modifier::BOLD),
+ )
+ // .highlight_symbol("> ")
+ .highlight_spacing(HighlightSpacing::Always);
+
+ // 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.entry_list.entry_list_state);
+ }
+
+ pub fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
+ // We get the info depending on the item's state.
+ // INFO: Only a placeholder at the moment:
+ let info = "Infor for selected item".to_string();
+ // TODO: Implement logic showin informations for selected entry:
+ // let info = if let Some(i) = self.tag_list.state.selected() {
+ // "Infor for selected item".to_string()
+ // // match self.todo_list.items[i].status {
+ // // Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
+ // // Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].info),
+ // // }
+ // } else {
+ // "Nothing selected...".to_string()
+ // };
+
+ // We show the list item's info under the list in this paragraph
+ let block = Block::bordered()
+ .title(Line::raw(" Item Info ").centered())
+ // .borders(Borders::TOP)
+ .border_set(symbols::border::ROUNDED)
+ .border_style(BOX_BORDER_STYLE_MAIN)
+ .bg(Color::Black)
+ .padding(Padding::horizontal(1));
+
+ // We can now render the item info
+ Paragraph::new(info)
+ .block(block)
+ .fg(TEXT_FG_COLOR)
+ .wrap(Wrap { trim: false })
+ .render(area, buf);
+ }
+
+ pub fn render_taglist(&mut self, area: Rect, buf: &mut Buffer) {
+ let block = Block::bordered()
+ .title(Line::raw(" Tag List ").centered())
+ .border_set(symbols::border::ROUNDED)
+ .border_style(BOX_BORDER_STYLE_MAIN)
+ .bg(Color::Black)
+ .padding(Padding::horizontal(1));
+
+ // Iterate through all elements in the `items` and stylize them.
+ let items: Vec<ListItem> = self
+ .tag_list
+ .tag_list_items
+ .iter()
+ .enumerate()
+ .map(|(i, todo_item)| {
+ let color = alternate_colors(i);
+ ListItem::from(todo_item).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);
+
+ // 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);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..d199c9e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,67 @@
+use std::{fs, io};
+
+use backend::cliargs::{self, CLIArgs};
+use ratatui::{backend::CrosstermBackend, Terminal};
+
+use crate::{
+ frontend::app::{App, AppResult},
+ frontend::event::{Event, EventHandler},
+ frontend::handler::handle_key_events,
+ frontend::tui::Tui,
+};
+
+use sarge::prelude::*;
+
+pub mod backend;
+pub mod frontend;
+
+#[tokio::main]
+async fn main() -> AppResult<()> {
+ // Parse CLI arguments
+ let parsed_args = CLIArgs::parse_cli_args();
+
+ // Print help if -h/--help flag is passed and exit
+ if parsed_args.helparg {
+ println!("{}", cliargs::help_func());
+ std::process::exit(0);
+ }
+
+ if parsed_args.versionarg {
+ // println!("Version Zero");
+ println!("{}", cliargs::version_func());
+ std::process::exit(0);
+ }
+ // TODO: Implement logic for CLI arguments/options which need to be handled
+ // before the TUI is started
+
+ // Create an application.
+ let mut app = App::new();
+
+ // TEST: Get Data from main bibliography
+ // let bibfile = fs::read_to_string("test.bib").unwrap();
+ // let biblio = Bibliography::parse(&bibfile).unwrap();
+
+ // Initialize the terminal user interface.
+ let backend = CrosstermBackend::new(io::stdout());
+ let terminal = Terminal::new(backend)?;
+ let events = EventHandler::new(250);
+ let mut tui = Tui::new(terminal, events);
+ tui.init()?;
+
+ // Start the main loop.
+ while app.running {
+ // Render the user interface.
+ tui.draw(&mut app)?;
+ // Handle events.
+ match tui.events.next().await? {
+ Event::Tick => app.tick(),
+ Event::Key(key_event) => handle_key_events(key_event, &mut app)?,
+ Event::Mouse(_) => {}
+ Event::Resize(_, _) => {}
+ }
+ }
+
+ // Exit the user interface.
+ tui.exit()?;
+ Ok(())
+}