// 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 .
/////
pub mod colors;
pub mod commands;
pub mod popup;
pub mod ui;
use crate::{cliargs::CLIArgs, App};
use crossterm::{
cursor,
event::{
DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, KeyEvent, MouseEvent,
},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
// use ratatui::backend::{Backend, CrosstermBackend};
use color_eyre::eyre::{OptionExt, Result};
use futures::{FutureExt, StreamExt};
use ratatui::backend::CrosstermBackend;
use std::io::{stdout, Stdout};
use std::panic;
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
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),
}
#[derive(Debug)]
pub struct Tui {
/// Interface to the Terminal.
pub terminal: ratatui::Terminal>,
/// Event sender channel.
evt_sender: mpsc::UnboundedSender,
/// Event receiver channel.
evt_receiver: mpsc::UnboundedReceiver,
/// Event handler thread.
handler: tokio::task::JoinHandle<()>,
cancellation_token: CancellationToken,
}
impl Tui {
// Constructs a new instance of [`Tui`].
pub fn new() -> Result {
let terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
let (evt_sender, evt_receiver) = mpsc::unbounded_channel();
let handler = tokio::spawn(async {});
let cancellation_token = CancellationToken::new();
Ok(Self {
terminal,
evt_sender,
evt_receiver,
handler,
cancellation_token,
})
}
pub fn start(&mut self) {
let tick_rate = Duration::from_millis(1000);
self.cancel();
self.cancellation_token = CancellationToken::new();
let event_loop = Self::event_loop(
self.evt_sender.clone(),
self.cancellation_token.clone(),
tick_rate,
);
// let _cancellation_token = self.cancellation_token.clone();
// let _sender = self.sender.clone();
self.handler = tokio::spawn(async {
event_loop.await;
});
}
async fn event_loop(
sender: mpsc::UnboundedSender,
cancellation_token: CancellationToken,
tick_rate: Duration,
) {
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();
}
};
}
cancellation_token.cancel();
}
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::init_error_hooks()?;
self.start();
Ok(())
}
pub fn cancel(&self) {
self.cancellation_token.cancel();
}
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.terminal.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(())
}
// [`Draw`] the terminal interface by [`rendering`] the widgets.
//
// [`Draw`]: ratatui::Terminal::draw
// [`rendering`]: crate::ui::render
pub fn draw(&mut self, app: &mut App, args: &CLIArgs) -> Result<()> {
// self.terminal.draw(|frame| ui::render(app, frame))?;
self.terminal
// .draw(|frame| frame.render_widget(app, frame.area()))?;
.draw(|frame| ui::render_ui(app, args, frame))?;
Ok(())
}
pub async fn next(&mut self) -> Result {
self.evt_receiver
.recv()
.await
.ok_or_eyre("This is an IO error")
}
}
impl Deref for Tui {
type Target = ratatui::Terminal>;
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();
}
}