diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/backend.rs | 2 | ||||
| -rw-r--r-- | src/backend/bib.rs | 51 | ||||
| -rw-r--r-- | src/backend/cliargs.rs | 89 | ||||
| -rw-r--r-- | src/frontend.rs | 5 | ||||
| -rw-r--r-- | src/frontend/app.rs | 221 | ||||
| -rw-r--r-- | src/frontend/event.rs | 97 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 70 | ||||
| -rw-r--r-- | src/frontend/tui.rs | 77 | ||||
| -rw-r--r-- | src/frontend/ui.rs | 201 | ||||
| -rw-r--r-- | src/main.rs | 67 |
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(()) +} |
