// Stdlib dependencies: pub(crate) use std::error::Error; pub(crate) use std::io::{stdout}; pub(crate) use std::thread::{spawn, JoinHandle}; pub(crate) use std::time::Duration; pub(crate) use std::collections::BTreeMap; pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; pub(crate) use std::sync::{ Arc, Mutex, MutexGuard, RwLock, LockResult, TryLockResult, RwLockReadGuard, RwLockWriteGuard }; // Non-stdlib dependencies: pub(crate) use microxdg::XdgApp; pub(crate) use ratatui::prelude::*; pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7}; pub(crate) use crossterm::{ExecutableCommand}; pub(crate) use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; use better_panic::{Settings, Verbosity}; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode }; /// Define and reexport submodules. #[macro_export] macro_rules! submod { ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; } submod!( handle midi render time ); /// Standard result type. pub type Usually = Result>; /// A UI component. pub trait Component: Render + Handle + Sync { /// Perform type erasure for collecting heterogeneous components. fn boxed (self) -> Box where Self: Sized + 'static { Box::new(self) } } /// Anything that implements `Render` + `Handle` can be used as a UI component. impl Component for T {} /// A UI component that may be associated with a JACK client by the `Jack` factory. pub trait Device: Render + Handle + Process + Send + Sync { /// Perform type erasure for collecting heterogeneous devices. fn boxed (self) -> Box where Self: Sized + 'static { Box::new(self) } } /// All things that implement the required traits can be treated as `Device`. impl Device for T {} // Reexport macros: pub use crate::{submod, render, handle, process, phrase, keymap, key, ports}; // Reexport JACK proto-lib: pub use crate::jack::*; impl Run for T {} pub trait Run: Render + Handle + Send + Sync + Sized + 'static { fn run (self, init: Option>)->Usually<()>>) -> Usually<()> { let app = Arc::new(RwLock::new(self)); let exited = Arc::new(AtomicBool::new(false)); let _input_thread = input_thread(&exited, &app); terminal_setup()?; panic_hook_setup(); let main_thread = main_thread(&exited, &app)?; if let Some(init) = init { init(app)?; } main_thread.join().unwrap(); terminal_teardown()?; Ok(()) } } /// Set up panic hook pub fn panic_hook_setup () { 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); })); } /// Set up terminal pub fn terminal_setup () -> Usually<()> { stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?; Ok(()) } /// Cleanup pub fn terminal_teardown () -> Usually<()> { stdout().execute(LeaveAlternateScreen)?; disable_raw_mode()?; Ok(()) } /// Main thread render loop pub fn main_thread ( exited: &Arc, device: &Arc> ) -> Usually> { let exited = exited.clone(); let device = device.clone(); let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; let sleep = Duration::from_millis(16); Ok(spawn(move || loop { terminal.draw(|frame|{ let area = frame.size(); let buffer = frame.buffer_mut(); device.read().unwrap() .render(buffer, area) .expect("Failed to render content"); }).expect("Failed to render frame"); if exited.fetch_and(true, Ordering::Relaxed) { break } std::thread::sleep(sleep); })) } /// Spawn thread that listens for user input pub fn input_thread ( exited: &Arc, device: &Arc> ) -> JoinHandle<()> { let poll = Duration::from_millis(100); let exited = exited.clone(); let device = device.clone(); spawn(move || loop { // Exit if flag is set if exited.fetch_and(true, Ordering::Relaxed) { break } // Listen for events and send them to the main thread if ::crossterm::event::poll(poll).is_ok() { let event = ::crossterm::event::read().unwrap(); match event { Event::Key(KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. }) => { exited.store(true, Ordering::Relaxed); }, _ => if device.write().unwrap().handle(&AppEvent::Input(event)).is_err() { break } } } }) }