diff options
| -rw-r--r-- | Cargo.lock | 15 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | src/frontend.rs | 1 | ||||
| -rw-r--r-- | src/frontend/app.rs | 26 | ||||
| -rw-r--r-- | src/frontend/event.rs | 111 | ||||
| -rw-r--r-- | src/frontend/handler.rs | 5 | ||||
| -rw-r--r-- | src/frontend/tui.rs | 234 | ||||
| -rw-r--r-- | src/main.rs | 7 |
8 files changed, 233 insertions, 168 deletions
@@ -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" @@ -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; |
