mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
222 lines
7.6 KiB
Rust
222 lines
7.6 KiB
Rust
use crate::*;
|
|
pub(crate) use ratatui::buffer::Cell;
|
|
pub(crate) use crossterm::{ExecutableCommand};
|
|
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
|
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
|
|
pub use ratatui::style::{Stylize, Modifier};
|
|
use ratatui::backend::{Backend, CrosstermBackend};
|
|
use std::io::Stdout;
|
|
use crossterm::terminal::{
|
|
EnterAlternateScreen, LeaveAlternateScreen,
|
|
enable_raw_mode, disable_raw_mode
|
|
};
|
|
|
|
submod! { tui_border tui_buffer tui_colors tui_layout }
|
|
|
|
pub struct Tui {
|
|
exited: Arc<AtomicBool>,
|
|
buffer: usize,
|
|
buffers: [Buffer;2],
|
|
backend: CrosstermBackend<Stdout>,
|
|
event: RwLock<Option<TuiEvent>>,
|
|
area: [u16;4],
|
|
}
|
|
|
|
impl Engine for Tui {
|
|
type Unit = u16;
|
|
type Area = [Self::Unit;4];
|
|
type HandleInput = Self;
|
|
type Handled = bool;
|
|
type RenderInput = Self;
|
|
type Rendered = Self::Area;
|
|
fn exited (&self) -> bool {
|
|
self.exited.fetch_and(true, Ordering::Relaxed)
|
|
}
|
|
fn setup (&mut self) -> Usually<()> {
|
|
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
|
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
|
stdout().execute(LeaveAlternateScreen).unwrap();
|
|
disable_raw_mode().unwrap();
|
|
better_panic_handler(info);
|
|
}));
|
|
stdout().execute(EnterAlternateScreen)?;
|
|
self.backend.hide_cursor()?;
|
|
enable_raw_mode().map_err(Into::into)
|
|
}
|
|
fn teardown (&mut self) -> Usually<()> {
|
|
stdout().execute(LeaveAlternateScreen)?;
|
|
self.backend.show_cursor()?;
|
|
disable_raw_mode().map_err(Into::into)
|
|
}
|
|
// FIXME
|
|
fn area (&self) -> Self::Area {
|
|
self.area
|
|
}
|
|
#[inline]
|
|
fn with_area (&mut self, x: u16, y: u16, w: u16, h: u16) -> &mut Self {
|
|
self.with_rect([x, y, w, h]);
|
|
self
|
|
}
|
|
}
|
|
impl Tui {
|
|
/// Run the main loop.
|
|
pub fn run <R: Component<Tui> + Sized + 'static> (
|
|
state: Arc<RwLock<R>>
|
|
) -> Usually<Arc<RwLock<R>>> {
|
|
let backend = CrosstermBackend::new(stdout());
|
|
let area = backend.size()?;
|
|
let mut engine = Self {
|
|
exited: Arc::new(AtomicBool::new(false)),
|
|
event: RwLock::new(None),
|
|
buffer: 0,
|
|
buffers: [Buffer::empty(area), Buffer::empty(area)],
|
|
backend,
|
|
area: area.xywh(),
|
|
};
|
|
engine.setup()?;
|
|
let engine = Arc::new(RwLock::new(engine));
|
|
let _input_thread = {
|
|
let engine = engine.clone();
|
|
let state = state.clone();
|
|
let poll = Duration::from_millis(100);
|
|
spawn(move || loop {
|
|
if ::crossterm::event::poll(poll).is_ok() {
|
|
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
|
|
match event {
|
|
key!(Ctrl-KeyCode::Char('c')) => {
|
|
engine.write().unwrap().exited.store(true, Ordering::Relaxed);
|
|
},
|
|
_ => {
|
|
*engine.write().unwrap().event.write().unwrap() = Some(event);
|
|
if let Err(e) = state.write().unwrap().handle(&*engine.read().unwrap()) {
|
|
panic!("{e}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if engine.read().unwrap().exited() {
|
|
break
|
|
}
|
|
//engine.read().unwrap().handle(&mut *state.write().unwrap()).expect("handle failed");
|
|
})
|
|
};
|
|
let main_thread = {
|
|
let engine = engine.clone();
|
|
let state = state.clone();
|
|
let sleep = Duration::from_millis(20);
|
|
spawn(move || loop {
|
|
if let (Ok(mut engine), Ok(state)) = (engine.write(), state.try_read()) {
|
|
if engine.exited() {
|
|
break
|
|
}
|
|
engine.area = engine.backend.size().expect("get size failed").xywh();
|
|
state.render(&mut engine).expect("render failed");
|
|
engine.flip();
|
|
}
|
|
std::thread::sleep(sleep);
|
|
})
|
|
};
|
|
main_thread.join().expect("main thread failed");
|
|
engine.write().unwrap().teardown()?;
|
|
Ok(state)
|
|
}
|
|
pub fn event (&self) -> TuiEvent {
|
|
self.event.read().unwrap().clone().unwrap()
|
|
}
|
|
pub fn buffer (&mut self) -> &mut Buffer {
|
|
&mut self.buffers[self.buffer]
|
|
}
|
|
fn flip (&mut self) {
|
|
let previous_buffer = &self.buffers[1 - self.buffer];
|
|
let current_buffer = &self.buffers[self.buffer];
|
|
let updates = previous_buffer.diff(current_buffer);
|
|
self.backend.draw(updates.into_iter()).expect("failed to render");
|
|
self.buffers[1 - self.buffer].reset();
|
|
self.buffer = 1 - self.buffer;
|
|
}
|
|
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
|
|
buffer_update(self.buffer(), area, callback)
|
|
}
|
|
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
|
|
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
|
}
|
|
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
|
|
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
|
}
|
|
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
|
|
self.buffer_update(area, &|cell,_,_|{
|
|
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
|
cell.underline_color = color;
|
|
})
|
|
}
|
|
pub fn fill_char (&mut self, area: [u16;4], c: char) {
|
|
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
|
}
|
|
pub fn make_dim (&mut self) {
|
|
for cell in self.buffer().content.iter_mut() {
|
|
cell.bg = ratatui::style::Color::Rgb(30,30,30);
|
|
cell.fg = ratatui::style::Color::Rgb(100,100,100);
|
|
cell.modifier = ratatui::style::Modifier::DIM;
|
|
}
|
|
}
|
|
pub fn blit (
|
|
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
|
|
) -> Perhaps<[u16;4]> {
|
|
let text = text.as_ref();
|
|
let buf = self.buffer();
|
|
if x < buf.area.width && y < buf.area.height {
|
|
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
|
|
}
|
|
Ok(Some([x, y, text.len() as u16, 1]))
|
|
}
|
|
#[inline]
|
|
pub fn alter_area (
|
|
&mut self, alter: impl Fn([u16;4])->[u16;4]
|
|
) -> &mut Self {
|
|
let [x, y, w, h] = alter(self.area.xywh());
|
|
self.with_area(x, y, w, h)
|
|
}
|
|
#[inline]
|
|
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
|
|
self.area = area;
|
|
self
|
|
}
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum TuiEvent {
|
|
/// Terminal input
|
|
Input(::crossterm::event::Event),
|
|
/// Update values but not the whole form.
|
|
Update,
|
|
/// Update the whole form.
|
|
Redraw,
|
|
/// Device gains focus
|
|
Focus,
|
|
/// Device loses focus
|
|
Blur,
|
|
// /// JACK notification
|
|
// Jack(JackEvent)
|
|
}
|
|
|
|
/// Rendering unit struct to Ratatui returns zero-sized [Area] at render coordinates.
|
|
impl Render<Tui> for () {
|
|
fn render (&self, to: &mut Tui) -> Perhaps<[u16;4]> {
|
|
Ok(Some([to.area.x(), to.area.y(), 0, 0]))
|
|
}
|
|
}
|
|
|
|
impl Area<u16> for Rect {
|
|
fn x (&self) -> u16 { self.x }
|
|
fn y (&self) -> u16 { self.y }
|
|
fn w (&self) -> u16 { self.width }
|
|
fn h (&self) -> u16 { self.height }
|
|
}
|
|
|
|
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
|
match (lower, upper) {
|
|
(true, true) => Some('█'),
|
|
(true, false) => Some('▄'),
|
|
(false, true) => Some('▀'),
|
|
_ => None
|
|
}
|
|
}
|