use std::io::Stdout;
use crossterm::event;
use crossterm::event::DisableMouseCapture;
use crossterm::event::EnableMouseCapture;
use crossterm::event::Event;
use crossterm::event::EventStream;
use crossterm::event::KeyCode;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use crossterm::event::KeyboardEnhancementFlags;
use crossterm::event::PopKeyboardEnhancementFlags;
use crossterm::event::PushKeyboardEnhancementFlags;
use crossterm::execute;
use crossterm::terminal::disable_raw_mode;
use crossterm::terminal::enable_raw_mode;
use crossterm::terminal::EnterAlternateScreen;
use crossterm::terminal::LeaveAlternateScreen;
use futures::StreamExt;
use ratatui::backend::CrosstermBackend;
use ratatui::Frame;
use ratatui::TerminalOptions;
use ratatui::Viewport;
use crate::Result;
use crate::TuiError;
#[derive(Debug, PartialEq, Eq)]
pub enum ApplicationEvent
{
NoAction,
Return(String),
ReturnNone,
}
pub trait ApplicationTrait<ERROR>
{
fn tick(
&mut self,
f_index: u8,
) -> std::result::Result<(), ERROR>;
fn draw(
&mut self,
f: &mut Frame,
) -> std::result::Result<(), ERROR>;
fn handle_key_event(
&mut self,
code: KeyCode,
modifiers: KeyModifiers,
) -> std::result::Result<ApplicationEvent, ERROR>;
fn needs_redraw(&mut self) -> bool;
}
#[derive(Debug)]
pub struct Application
{
fullscreen: bool,
terminal_lines: u16,
terminal: Option<ratatui::Terminal<CrosstermBackend<Stdout>>>,
response: ApplicationEvent,
error_message: Option<String>,
default_keys: bool,
running: bool,
fps: u8,
current_frame: u8,
requires_keyboard_enhancements: bool,
}
impl std::default::Default for Application
{
fn default() -> Self
{
Self::new()
}
}
impl Application
{
pub fn new() -> Self
{
Self {
fullscreen: false,
terminal_lines: 8,
terminal: None,
response: ApplicationEvent::ReturnNone,
error_message: None,
default_keys: true,
running: false,
fps: 60,
current_frame: 1,
requires_keyboard_enhancements: false,
}
}
#[allow(dead_code)]
pub fn with_keyboard_enhancements(mut self) -> Self
{
self.requires_keyboard_enhancements = true;
self
}
#[allow(dead_code)]
pub fn with_fullscreen(mut self) -> Self
{
self.fullscreen = true;
self
}
#[allow(dead_code)]
pub fn with_fps(
mut self,
fps: u8,
) -> Self
{
self.fps = fps;
self
}
#[allow(dead_code)]
pub fn with_terminal_lines(
mut self,
terminal_lines: u16,
) -> Self
{
self.fullscreen = false;
self.terminal_lines = terminal_lines;
self
}
#[allow(dead_code)]
pub fn without_default_keys(mut self) -> Self
{
self.default_keys = false;
self
}
#[allow(dead_code)]
fn clear(&mut self) -> Result<()>
{
if let Some(terminal) = self
.terminal
.as_mut()
{
terminal.clear()?;
}
Ok(())
}
pub fn check_need_redraw<A, ERROR>(
&mut self,
app: &mut A,
) -> bool
where
A: ApplicationTrait<ERROR>,
{
if app.needs_redraw()
{
match self.clear()
{
Ok(_) => (),
Err(e) =>
{
self.error_message = Some(format!("Error {e}"));
self.running = false;
return false;
}
};
}
true
}
pub fn init(mut self) -> Self
{
if self.requires_keyboard_enhancements
{
let has_enhancements = match crossterm::terminal::supports_keyboard_enhancement()
{
Ok(value) => value,
Err(e) =>
{
self.error_message = Some(format!("Error {e}"));
return self;
}
};
if !has_enhancements
{
self.error_message = Some("Terminal must have keyboard enhancements. See https://sw.kovidgoyal.net/kitty/keyboard-protocol/".to_string());
return self;
}
}
if let Err(e) = enable_raw_mode()
{
self.error_message = Some(format!("Error {e}"));
return self;
};
let mut stdout = std::io::stdout();
if self.fullscreen
{
if let Err(e) = execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture
)
{
self.error_message = Some(format!("Error {e}"));
return self;
}
}
if self.requires_keyboard_enhancements
{
if let Err(e) = crossterm::execute!(
std::io::stdout(),
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
)
{
self.error_message = Some(format!("Error {e}"));
return self;
}
}
let backend = CrosstermBackend::new(stdout);
let terminal = match ratatui::Terminal::with_options(
backend,
TerminalOptions {
viewport: if self.fullscreen
{
Viewport::Fullscreen
}
else
{
Viewport::Inline(self.terminal_lines)
},
},
)
{
Err(e) =>
{
self.error_message = Some(format!("Error {e}"));
return self;
}
Ok(value) => value,
};
self.terminal = Some(terminal);
self
}
fn draw<A, ERROR>(
&mut self,
app: &mut A,
) -> bool
where
A: ApplicationTrait<ERROR>,
ERROR: std::fmt::Display,
{
self.current_frame += 1;
if self.current_frame == self.fps
{
self.current_frame = 0;
}
if let Err(e) = app.tick(self.current_frame)
{
self.error_message = Some(format!("Error {e}"));
self.running = false;
return false;
}
let mut err = None;
if let Err(e) = self
.terminal
.as_mut()
.unwrap()
.draw(
|f| {
if let Err(e) = app.draw(f)
{
err = Some(e.to_string());
}
},
)
{
self.error_message = Some(format!("Error {e}"));
self.running = false;
return false;
};
if let Some(err) = err
{
self.error_message = Some(err);
self.running = false;
return false;
}
true
}
fn handle_event<A, ERROR>(
&mut self,
app: &mut A,
event: &Event,
) where
A: ApplicationTrait<ERROR>,
ERROR: std::fmt::Display,
{
if let Event::Key(key) = event
{
if key.kind == KeyEventKind::Press
{
use KeyCode::*;
if self.default_keys && key.code == Esc
{
self.response = ApplicationEvent::ReturnNone;
self.running = false;
}
else
{
match app.handle_key_event(
key.code,
key.modifiers,
)
{
Err(e) =>
{
self.error_message = Some(format!("Error {e}"));
self.running = false;
}
Ok(ApplicationEvent::Return(value)) =>
{
self.response = ApplicationEvent::Return(value);
self.running = false;
}
Ok(ApplicationEvent::ReturnNone) =>
{
self.response = ApplicationEvent::ReturnNone;
self.running = false;
}
Ok(ApplicationEvent::NoAction) =>
{}
};
}
}
}
}
fn is_initialized(&self) -> bool
{
if self
.terminal
.is_none()
{
return false;
}
if self
.error_message
.is_some()
{
return false;
}
true
}
pub fn run<A, ERROR>(
mut self,
app: &mut A,
) -> Self
where
A: ApplicationTrait<ERROR>,
ERROR: std::fmt::Display,
{
if !self.is_initialized()
{
return self;
}
self.running = true;
while self.running
{
if !self.check_need_redraw(app)
{
continue;
}
if !self.draw(app)
{
continue;
}
match event::read()
{
Ok(event) => self.handle_event(
app, &event,
),
Err(e) =>
{
self.error_message = Some(format!("Error {e}"));
self.running = false;
}
};
}
self
}
#[allow(dead_code)]
pub async fn run_async<A, ERROR>(
mut self,
app: &mut A,
) -> Self
where
A: ApplicationTrait<ERROR>,
ERROR: std::fmt::Display,
{
if !self.is_initialized()
{
return self;
}
self.running = true;
let frames_per_second = self.fps as f32;
let period = std::time::Duration::from_secs_f32(1.0 / frames_per_second);
let mut interval = tokio::time::interval(period);
let mut events = EventStream::new();
while self.running
{
tokio::select! {
_ = interval.tick() => {
if !self.check_need_redraw(app) {
continue;
}
self.draw(app);
},
Some(Ok(event)) = events.next() => { self.handle_event(app, &event); }
}
}
self
}
pub fn on_exit(mut self) -> Result<ApplicationEvent>
{
if self
.terminal
.is_none()
{
if let Some(e) = self
.error_message
.clone()
{
return Err(TuiError::Application(e));
}
else
{
return Err(TuiError::Application("Terminal must be initialized".to_string()));
}
}
crossterm::execute!(
std::io::stdout(),
crossterm::cursor::Show,
crossterm::cursor::SetCursorStyle::DefaultUserShape
)?;
if self.requires_keyboard_enhancements
{
crossterm::execute!(
std::io::stdout(),
PopKeyboardEnhancementFlags
)?;
}
disable_raw_mode()?;
if self.fullscreen
{
execute!(
self.terminal
.as_mut()
.unwrap()
.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
self.terminal
.as_mut()
.unwrap()
.show_cursor()?;
}
else
{
self.terminal
.as_mut()
.unwrap()
.clear()?;
}
if let Some(e) = self
.error_message
.clone()
{
Err(TuiError::Application(e))
}
else
{
Ok(self.response)
}
}
}