aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/errorsetup.rs51
-rw-r--r--src/frontend/app.rs36
-rw-r--r--src/frontend/entries.rs55
-rw-r--r--src/frontend/handler.rs37
-rw-r--r--src/frontend/keywords.rs20
-rw-r--r--src/frontend/tui.rs2
-rw-r--r--src/frontend/ui.rs88
-rw-r--r--src/main.rs7
8 files changed, 253 insertions, 43 deletions
diff --git a/src/errorsetup.rs b/src/errorsetup.rs
new file mode 100644
index 0000000..eaaba0f
--- /dev/null
+++ b/src/errorsetup.rs
@@ -0,0 +1,51 @@
+// 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 <https://www.gnu.org/licenses/>.
+/////
+
+use color_eyre::config::HookBuilder;
+use color_eyre::eyre::Result;
+use crossterm::cursor;
+use crossterm::event::DisableMouseCapture;
+use crossterm::terminal::LeaveAlternateScreen;
+use std::io::stdout;
+
+// Define error hooks to restore the terminal after panic
+pub fn init_error_hooks() -> Result<()> {
+ let (panic, error) = HookBuilder::default().into_hooks();
+ let panic = panic.into_panic_hook();
+ let error = error.into_eyre_hook();
+ color_eyre::eyre::set_hook(Box::new(move |e| {
+ let _ = crossterm::execute!(
+ stdout(),
+ DisableMouseCapture,
+ LeaveAlternateScreen,
+ cursor::Show
+ );
+ let _ = crossterm::terminal::disable_raw_mode();
+ error(e)
+ }))?;
+ std::panic::set_hook(Box::new(move |info| {
+ let _ = crossterm::execute!(
+ stdout(),
+ DisableMouseCapture,
+ LeaveAlternateScreen,
+ cursor::Show
+ );
+ let _ = crossterm::terminal::disable_raw_mode();
+ panic(info)
+ }));
+ Ok(())
+}
diff --git a/src/frontend/app.rs b/src/frontend/app.rs
index 0751da1..ee2ab05 100644
--- a/src/frontend/app.rs
+++ b/src/frontend/app.rs
@@ -33,6 +33,7 @@ pub enum CurrentArea {
TagArea,
SearchArea,
HelpArea,
+ InfoArea,
}
// Check which area was active when popup set active
@@ -137,11 +138,20 @@ impl App {
// 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_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.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());
}
}
@@ -165,15 +175,25 @@ impl App {
}
pub fn scroll_info_down(&mut self) {
- self.scroll_info = self.scroll_info + 1;
+ // self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll + 1;
+ 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) {
- if self.scroll_info == 0 {
- {}
- } else {
- self.scroll_info = self.scroll_info - 1;
- }
+ // if self.entry_table.entry_info_scroll == 0 {
+ // {}
+ // } else {
+ // self.entry_table.entry_info_scroll = self.entry_table.entry_info_scroll - 1;
+ // }
+ 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
diff --git a/src/frontend/entries.rs b/src/frontend/entries.rs
index 1f79bf4..081de4c 100644
--- a/src/frontend/entries.rs
+++ b/src/frontend/entries.rs
@@ -22,12 +22,22 @@ use color_eyre::eyre::{Context, Ok, Result};
use core::panic;
use editor_command::EditorBuilder;
use itertools::Itertools;
-use ratatui::widgets::TableState;
+use ratatui::widgets::{ScrollbarState, TableState};
use std::process::{Command, Stdio};
+// Define list containing entries as table
+#[derive(Debug)]
+pub struct EntryTable {
+ pub entry_table_items: Vec<EntryTableItem>,
+ pub entry_table_state: TableState,
+ pub entry_scroll_state: ScrollbarState,
+ pub entry_info_scroll: u16,
+ pub entry_info_scroll_state: ScrollbarState,
+}
+
impl FromIterator<Vec<String>> for EntryTable {
fn from_iter<T: IntoIterator<Item = Vec<String>>>(iter: T) -> Self {
- let entry_table_items = iter
+ let entry_table_items: Vec<EntryTableItem> = iter
.into_iter()
.sorted()
// 0: authors, 1: title, 2: date, 3: pubtype, 4: keywords, 5: citekey
@@ -40,20 +50,18 @@ impl FromIterator<Vec<String>> for EntryTable {
})
.collect();
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_state,
+ entry_scroll_state,
+ entry_info_scroll: 0,
+ entry_info_scroll_state,
}
}
}
-// Define list containing entries as table
-#[derive(Debug)]
-pub struct EntryTable {
- pub entry_table_items: Vec<EntryTableItem>,
- pub entry_table_state: TableState,
-}
-
// Define contents of each entry table row
#[derive(Debug)]
pub struct EntryTableItem {
@@ -125,23 +133,44 @@ impl App {
// Movement
pub fn select_next_entry(&mut self) {
- self.scroll_info = 0;
+ 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_next();
+ 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) {
- self.scroll_info = 0;
+ 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_previous();
+ 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.scroll_info = 0;
+ 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.scroll_info = 0;
+ 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());
}
// Get the citekey of the selected entry
diff --git a/src/frontend/handler.rs b/src/frontend/handler.rs
index 9a27b3a..c2cacf5 100644
--- a/src/frontend/handler.rs
+++ b/src/frontend/handler.rs
@@ -48,12 +48,26 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R
match app.current_area {
// Keycodes for the tag area
CurrentArea::TagArea => match key_event.code {
- KeyCode::Char('j') | KeyCode::Down => {
+ KeyCode::Down => {
app.select_next_tag();
}
- KeyCode::Char('k') | KeyCode::Up => {
+ KeyCode::Up => {
app.select_previous_tag();
}
+ KeyCode::Char('j') => {
+ if key_event.modifiers == KeyModifiers::ALT {
+ app.scroll_info_down();
+ } else {
+ app.select_next_tag();
+ }
+ }
+ KeyCode::Char('k') => {
+ if key_event.modifiers == KeyModifiers::ALT {
+ app.scroll_info_up();
+ } else {
+ app.select_previous_tag();
+ }
+ }
KeyCode::Char('g') | KeyCode::Home => {
app.select_first_tag();
}
@@ -81,12 +95,26 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R
},
// Keycodes for the entry area
CurrentArea::EntryArea => match key_event.code {
- KeyCode::Char('j') | KeyCode::Down => {
+ KeyCode::Down => {
app.select_next_entry();
}
- KeyCode::Char('k') | KeyCode::Up => {
+ KeyCode::Up => {
app.select_previous_entry();
}
+ KeyCode::Char('j') => {
+ if key_event.modifiers == KeyModifiers::ALT {
+ app.scroll_info_down();
+ } else {
+ app.select_next_entry();
+ }
+ }
+ KeyCode::Char('k') => {
+ if key_event.modifiers == KeyModifiers::ALT {
+ app.scroll_info_up();
+ } else {
+ app.select_previous_entry();
+ }
+ }
KeyCode::Char('g') | KeyCode::Home => {
app.select_first_entry();
}
@@ -148,6 +176,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App, tui: &mut Tui) -> R
}
_ => {}
},
+ CurrentArea::InfoArea => {}
}
Ok(())
}
diff --git a/src/frontend/keywords.rs b/src/frontend/keywords.rs
index 4beec14..6aa4068 100644
--- a/src/frontend/keywords.rs
+++ b/src/frontend/keywords.rs
@@ -18,12 +18,13 @@
use super::app::{App, FormerArea};
use super::entries::EntryTable;
use crate::backend::search::BibiSearch;
-use ratatui::widgets::ListState;
+use ratatui::widgets::{ListState, ScrollbarState};
#[derive(Debug)]
pub struct TagList {
pub tag_list_items: Vec<TagListItem>,
pub tag_list_state: ListState,
+ pub tag_scroll_state: ScrollbarState,
}
// Structure of the list items.
@@ -43,14 +44,16 @@ impl TagListItem {
impl FromIterator<String> for TagList {
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
- let tag_list_items = iter
+ let tag_list_items: Vec<TagListItem> = iter
.into_iter()
.map(|info| TagListItem::new(&info))
.collect();
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,
}
}
}
@@ -61,18 +64,31 @@ impl App {
// Movement
pub fn select_next_tag(&mut self) {
self.tag_list.tag_list_state.select_next();
+ 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) {
self.tag_list.tag_list_state.select_previous();
+ 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 {
diff --git a/src/frontend/tui.rs b/src/frontend/tui.rs
index a4560c1..698407d 100644
--- a/src/frontend/tui.rs
+++ b/src/frontend/tui.rs
@@ -179,7 +179,7 @@ impl Tui {
// if self.paste {
// crossterm::execute!(stdout(), EnableBracketedPaste)?;
// }
- Self::init_error_hooks()?;
+ // Self::init_error_hooks()?;
self.start();
Ok(())
}
diff --git a/src/frontend/ui.rs b/src/frontend/ui.rs
index 6a292d7..7edcf43 100644
--- a/src/frontend/ui.rs
+++ b/src/frontend/ui.rs
@@ -17,13 +17,14 @@
use ratatui::{
buffer::Buffer,
- layout::{Constraint, Layout, Rect},
+ layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols,
text::{Line, Span, Text},
widgets::{
- Block, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, StatefulWidget,
- Table, Widget, Wrap,
+ block::{Position, Title},
+ Block, Cell, HighlightSpacing, List, ListItem, Padding, Paragraph, Row, Scrollbar,
+ ScrollbarOrientation, StatefulWidget, Table, Widget, Wrap,
},
};
@@ -47,6 +48,9 @@ const SELECTED_STYLE: Style = Style::new()
const TEXT_FG_COLOR: Color = Color::Indexed(252);
const TEXT_UNSELECTED_FG_COLOR: Color = Color::Indexed(245);
+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
@@ -83,7 +87,6 @@ impl Widget for &mut App {
// Render list area where entry gets selected
self.render_entrytable(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);
}
@@ -228,6 +231,24 @@ impl App {
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(SCROLLBAR_LOWER_CORNER)
+ .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) {
@@ -323,22 +344,44 @@ impl App {
.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.scroll_info == 0 {
- self.scroll_info
+ if self.entry_table.entry_info_scroll == 0 {
+ self.entry_table.entry_info_scroll
} else if area.height > box_height as u16 {
- self.scroll_info = 0;
- self.scroll_info
- } else if self.scroll_info > (box_height as u16 + 1 - area.height) {
- self.scroll_info = box_height as u16 + 1 - area.height;
- self.scroll_info
+ 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.scroll_info
+ self.entry_table.entry_info_scroll
}
};
// We can now render the item info
Paragraph::new(info)
- .block(block)
+ .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))
@@ -389,8 +432,27 @@ impl App {
// .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 74e1252..6ef7ee1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,13 +15,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/////
-use core::panic;
-
use backend::cliargs::{self, CLIArgs};
use color_eyre::eyre::Result;
+use core::panic;
+use errorsetup::init_error_hooks;
use frontend::app::App;
pub mod backend;
+pub mod errorsetup;
pub mod frontend;
#[tokio::main]
@@ -45,6 +46,8 @@ async fn main() -> Result<()> {
panic!("No \'.bib\' file passed, aborting")
}
+ init_error_hooks()?;
+
// Create an application.
let mut app = App::new(parsed_args)?;