aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock15
-rw-r--r--Cargo.toml2
-rw-r--r--src/frontend.rs1
-rw-r--r--src/frontend/app.rs26
-rw-r--r--src/frontend/event.rs111
-rw-r--r--src/frontend/handler.rs5
-rw-r--r--src/frontend/tui.rs234
-rw-r--r--src/main.rs7
8 files changed, 233 insertions, 168 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 36a9032..4cebb38 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -104,7 +104,9 @@ dependencies = [
"ratatui",
"regex",
"sarge",
+ "signal-hook",
"tokio",
+ "tokio-util",
]
[[package]]
@@ -1346,6 +1348,19 @@ dependencies = [
]
[[package]]
+name = "tokio-util"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 9b182eb..3ef2932 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,4 +16,6 @@ nucleo-matcher = "0.3.1"
ratatui = { version = "0.28.1", features = ["unstable-rendered-line-info"]}
regex = "1.10.6"
sarge = "7.2.5"
+signal-hook = "0.3.17"
tokio = { version = "1.39.3", features = ["full"] }
+tokio-util = "0.7.12"
diff --git a/src/frontend.rs b/src/frontend.rs
index 9ee632a..a03c096 100644
--- a/src/frontend.rs
+++ b/src/frontend.rs
@@ -16,7 +16,6 @@
/////
pub mod app;
-pub mod event;
pub mod handler;
pub mod tui;
pub mod ui;
diff --git a/src/frontend/app.rs b/src/frontend/app.rs
index 0e1c9b9..4e32fe7 100644
--- a/src/frontend/app.rs
+++ b/src/frontend/app.rs
@@ -22,9 +22,8 @@ use ratatui::{backend::CrosstermBackend, Terminal};
use crate::backend::{bib::*, search::BibiSearch};
use crate::{
- frontend::event::{Event, EventHandler},
frontend::handler::handle_key_events,
- frontend::tui::Tui,
+ frontend::tui::{Event, Tui},
};
use std::{error, net::SocketAddr};
@@ -61,7 +60,7 @@ pub struct App {
// Is the application running?
pub running: bool,
// // tui initialization
- // pub tui: Tui,
+ pub tui: Tui,
// main bibliography
pub main_biblio: BibiMain,
// bibliographic data
@@ -228,7 +227,7 @@ impl App {
pub fn new() -> Result<Self> {
// Self::default()
let running = true;
- // let tui = Tui::new()?;
+ let tui = Tui::new()?;
let main_biblio = BibiMain::new();
let biblio_data = BibiData::new(&main_biblio.bibliography, &main_biblio.citekeys);
let tag_list = TagList::from_iter(main_biblio.keyword_list.clone());
@@ -237,7 +236,7 @@ impl App {
let current_area = CurrentArea::EntryArea;
Ok(Self {
running,
- // tui,
+ tui,
main_biblio,
biblio_data,
tag_list,
@@ -256,14 +255,14 @@ impl App {
// let terminal = Terminal::new(backend)?;
// let events = EventHandler::new(250);
let mut tui = tui::Tui::new()?;
- tui.init()?;
+ tui.enter()?;
// Start the main loop.
while self.running {
// Render the user interface.
tui.draw(self)?;
// Handle events.
- match tui.events.next().await? {
+ match tui.next().await? {
Event::Tick => self.tick(),
Event::Key(key_event) => handle_key_events(key_event, self)?,
Event::Mouse(_) => {}
@@ -518,4 +517,17 @@ impl App {
let citekey = &self.entry_table.entry_table_items[idx].citekey;
citekey
}
+
+ pub fn run_editor(&mut self) -> Result<()> {
+ self.tui.exit()?;
+ let cmd = String::from("hx");
+ let args: Vec<String> = vec!["test.bib".into()];
+ let status = std::process::Command::new(&cmd).args(&args).status()?;
+ if !status.success() {
+ eprintln!("Spawning editor failed with status {}", status);
+ }
+ self.tui.enter()?;
+ self.tui.terminal.clear()?;
+ Ok(())
+ }
}
diff --git a/src/frontend/event.rs b/src/frontend/event.rs
deleted file mode 100644
index 65b61f1..0000000
--- a/src/frontend/event.rs
+++ /dev/null
@@ -1,111 +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 <https://www.gnu.org/licenses/>.
-/////
-
-use std::time::Duration;
-
-use color_eyre::eyre::{OptionExt, Result};
-use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
-use futures::{FutureExt, StreamExt};
-use tokio::sync::mpsc;
-
-/// 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) -> Result<Event> {
- self.receiver.recv().await.ok_or_eyre("This is an IO error")
- // .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
index 11c2652..99eaa8e 100644
--- a/src/frontend/handler.rs
+++ b/src/frontend/handler.rs
@@ -18,7 +18,7 @@
use crate::frontend::app::App;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-use super::app::{CurrentArea, FormerArea};
+use super::app::CurrentArea;
use color_eyre::eyre::Result;
/// Handles the key events and updates the state of [`App`].
@@ -95,6 +95,9 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> Result<()> {
KeyCode::Char('y') => {
App::yank_text(&app.get_selected_citekey());
}
+ KeyCode::Char('e') => {
+ app.run_editor()?;
+ }
KeyCode::Char('/') => {
app.enter_search_area();
}
diff --git a/src/frontend/tui.rs b/src/frontend/tui.rs
index add3e56..f3612b2 100644
--- a/src/frontend/tui.rs
+++ b/src/frontend/tui.rs
@@ -16,12 +16,16 @@
/////
use crate::frontend::app::App;
-use crate::frontend::event::EventHandler;
-use color_eyre::eyre::Result;
-use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
-use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
-use ratatui::backend::{Backend, CrosstermBackend};
-// use ratatui::backend::CrosstermBackend as Backend;
+use crossterm::{
+ cursor,
+ event::{
+ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
+ Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind, MouseEvent,
+ },
+ terminal::{EnterAlternateScreen, LeaveAlternateScreen},
+};
+// use ratatui::backend::{Backend, CrosstermBackend};
+use ratatui::backend::CrosstermBackend as Backend;
use ratatui::Terminal;
use std::io::{self, stdout, Stdout};
use std::panic;
@@ -30,6 +34,24 @@ use std::{
time::Duration,
};
+use color_eyre::eyre::{OptionExt, Result};
+use futures::{FutureExt, StreamExt};
+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),
+}
+
// pub type IO = std::io::{{crossterm_io | title_case}};
// pub fn io() -> IO {
// std::io::{{crossterm_io}}()
@@ -41,37 +63,137 @@ use std::{
#[derive(Debug)]
pub struct Tui {
/// Interface to the Terminal.
- terminal: ratatui::Terminal<CrosstermBackend<Stdout>>,
- /// Terminal event handler.
- pub events: EventHandler,
+ pub terminal: ratatui::Terminal<Backend<Stdout>>,
+ /// Event sender channel.
+ sender: mpsc::UnboundedSender<Event>,
+ /// Event receiver channel.
+ receiver: mpsc::UnboundedReceiver<Event>,
+ /// Event handler thread.
+ handler: tokio::task::JoinHandle<()>,
+ cancellation_token: CancellationToken,
}
impl Tui {
/// Constructs a new instance of [`Tui`].
pub fn new() -> Result<Self> {
- let backend = CrosstermBackend::new(stdout());
- let terminal = Terminal::new(backend)?;
- let events = EventHandler::new(250);
- Ok(Self { terminal, events })
+ 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.cancellation_token = CancellationToken::new();
+ let _cancellation_token = self.cancellation_token.clone();
+ let _sender = self.sender.clone();
+ self.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;
+ // }
+ _ = _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();
+ }
+ };
+ }
+ });
}
/// Initializes the terminal interface.
///
/// It enables the raw mode and sets terminal properties.
- pub fn init(&mut self) -> Result<()> {
- 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()?;
+ // pub fn init(&mut self) -> Result<()> {
+ // 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(())
+ // }
+
+ 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.start();
+ Ok(())
+ }
+
+ 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(())
}
@@ -86,22 +208,50 @@ impl Tui {
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() -> Result<()> {
- terminal::disable_raw_mode()?;
- crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
- 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() -> Result<()> {
+ // 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) -> Result<()> {
+ // Self::reset()?;
+ // self.terminal.show_cursor()?;
+ // Ok(())
+ // }
+
+ pub async fn next(&mut self) -> Result<Event> {
+ self.receiver.recv().await.ok_or_eyre("This is an IO error")
+ // .ok_or(Box::new(std::io::Error::new(
+ // std::io::ErrorKind::Other,
+ // "This is an IO error",
+ // )))
}
+}
- /// Exits the terminal interface.
- ///
- /// It disables the raw mode and reverts back the terminal properties.
- pub fn exit(&mut self) -> Result<()> {
- Self::reset()?;
- self.terminal.show_cursor()?;
- Ok(())
+impl Deref for Tui {
+ type Target = ratatui::Terminal<Backend<Stdout>>;
+
+ 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/main.rs b/src/main.rs
index 9fc9d17..ba242b7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,12 +20,7 @@ use std::io;
use backend::cliargs::{self, CLIArgs};
use ratatui::{backend::CrosstermBackend, Terminal};
-use crate::{
- frontend::app::App,
- frontend::event::{Event, EventHandler},
- frontend::handler::handle_key_events,
- frontend::tui::Tui,
-};
+use crate::{frontend::app::App, frontend::handler::handle_key_events, frontend::tui::Tui};
use color_eyre::eyre::Result;