big ass refactor (rip client)

This commit is contained in:
🪞👃🪞 2024-07-03 14:51:48 +03:00
parent 94c1f83ef2
commit 8c3cf53c67
56 changed files with 2232 additions and 1891 deletions

View file

@ -1,5 +1,9 @@
use crate::core::*;
pub trait Component: Render + Handle {}
impl<T: Render + Handle> Component for T {}
/// A UI component that may have presence on the JACK grap.
pub trait Device: Render + Handle + PortList + Send + Sync {
fn boxed (self) -> Box<dyn Device> where Self: Sized + 'static {
@ -25,7 +29,7 @@ impl<T> Handle for DynamicDevice<T> {
}
}
impl<T> Render for DynamicDevice<T> {
impl<T: Send> Render for DynamicDevice<T> {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
self.render.lock().unwrap()(&*self.state.lock().unwrap(), buf, area)
}

View file

@ -10,6 +10,26 @@ pub trait Handle {
}
}
#[macro_export] macro_rules! handle {
($T:ty) => {
impl Handle for $T {}
};
($T:ty |$self:ident, $e:ident|$block:tt) => {
impl Handle for $T {
fn handle (&mut $self, $e: &AppEvent) -> Usually<bool> {
$block
}
}
};
($T:ty = $handle:path) => {
impl Handle for $T {
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
$handle(self, e)
}
}
}
}
#[derive(Debug)]
pub enum AppEvent {
/// Terminal input

View file

@ -1,7 +1,51 @@
use crate::core::*;
pub fn jack_run <T> (name: &str, app: &Arc<Mutex<T>>) -> Usually<DynamicAsyncClient>
where T: Handle + Process + Send + 'static
{
let options = ClientOptions::NO_START_SERVER;
let (client, _status) = Client::new(name, options)?;
Ok(client.activate_async(
Notifications(Box::new({
let _app = app.clone();
move|_event|{
// FIXME: this deadlocks
//app.lock().unwrap().handle(&event).unwrap();
}
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
ClosureProcessHandler::new(Box::new({
let app = app.clone();
move|c: &Client, s: &ProcessScope|{
app.lock().unwrap().process(c, s)
}
}) as BoxedProcessHandler)
)?)
}
pub trait Process {
fn process (&mut self, c: &Client, s: &ProcessScope) -> Control;
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}
}
#[macro_export] macro_rules! process {
($T:ty) => {
impl Process for $T {}
};
($T:ty |$self:ident, $c:ident, $s:ident|$block:tt) => {
impl Process for $T {
fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control {
$block
}
}
};
($T:ty = $process:path) => {
impl Process for $T {
fn process (&mut self, c: &Client, s: &ProcessScope) -> Control {
$process(self, c, s)
}
}
}
}
pub type DynamicAsyncClient =
@ -16,23 +60,6 @@ pub type DynamicProcessHandler =
pub type BoxedProcessHandler =
Box<dyn FnMut(&Client, &ProcessScope)-> Control + Send>;
pub fn jack_run <T> (name: &str, app: &Arc<Mutex<T>>) -> Usually<DynamicAsyncClient>
where T: Handle + Process + Send + 'static
{
let options = ClientOptions::NO_START_SERVER;
let (client, _status) = Client::new(name, options)?;
Ok(client.activate_async(
Notifications(Box::new({
let app = app.clone();
move|event|{app.lock().unwrap().handle(&event).unwrap();}
}) as Box<dyn Fn(AppEvent) + Send + Sync>),
ClosureProcessHandler::new(Box::new({
let app = app.clone();
move|c: &Client, s: &ProcessScope|{app.lock().unwrap().process(c, s)}
}) as BoxedProcessHandler)
)?)
}
pub use ::jack::{
AsyncClient,
AudioIn,

36
src/core/midi.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::core::*;
pub type PhraseData =
BTreeMap<usize, Vec<MidiMessage>>;
pub type MIDIMessage =
Vec<u8>;
pub type MIDIChunk =
[Option<Vec<MIDIMessage>>];
pub const KEY_WHITE: Style = Style {
fg: Some(Color::Gray),
bg: None,
underline_color: None,
add_modifier: ::ratatui::style::Modifier::empty(),
sub_modifier: ::ratatui::style::Modifier::empty(),
};
pub const KEY_BLACK: Style = Style {
fg: Some(Color::Black),
bg: None,
underline_color: None,
add_modifier: ::ratatui::style::Modifier::empty(),
sub_modifier: ::ratatui::style::Modifier::empty(),
};
pub const KEY_STYLE: [Style;12] = [
KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE,
KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE, KEY_BLACK, KEY_WHITE,
];
pub const KEYS_VERTICAL: [&'static str; 6] = [
"", "", "", "", "", "",
];

View file

@ -1,20 +0,0 @@
pub use std::error::Error;
pub use std::io::{stdout, Stdout, Write};
pub use std::thread::{spawn, JoinHandle};
pub use std::time::Duration;
pub use std::collections::BTreeMap;
pub use std::sync::{Arc, Mutex, MutexGuard};
pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
pub use std::sync::mpsc::{channel, Sender, Receiver};
pub use ratatui::prelude::*;
pub use midly::{MidiMessage, live::LiveEvent, num::u7};
pub use crossterm::{ExecutableCommand, QueueableCommand};
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
macro_rules! submod { ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; }
submod!( device handle jack keymap port render run time );
pub type Usually<T> = Result<T, Box<dyn Error>>;
pub use crate::{key, keymap};

View file

@ -2,34 +2,38 @@ use crate::core::*;
use ratatui::widgets::WidgetRef;
/// Trait for things that render to the display.
pub trait Render {
pub trait Render: Send {
// Render something to an area of the buffer.
// Returns area used by component.
// This is insufficient but for the most basic dynamic layout algorithms.
fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually<Rect> {
Ok(Rect { x: 0, y: 0, width: 0, height: 0 })
}
fn min_width (&self) -> u16 {
0
}
fn max_width (&self) -> u16 {
u16::MAX
}
fn min_height (&self) -> u16 {
0
}
fn max_height (&self) -> u16 {
u16::MAX
}
//fn boxed (self) -> Box<dyn Render> where Self: Sized + 'static {
//Box::new(self)
//}
}
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect>> Render for T {
#[macro_export] macro_rules! render {
($T:ty) => {
impl Render for $T {}
};
($T:ty |$self:ident, $buf:ident, $area:ident|$block:tt) => {
impl Render for $T {
fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually<Rect> {
$block
}
}
};
($T:ty = $render:path) => {
impl Render for $T {
fn render (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
$render(self, buf, area)
}
}
}
}
impl<T: Fn(&mut Buffer, Rect) -> Usually<Rect> + Send> Render for T {
fn render (&self, b: &mut Buffer, a: Rect) -> Usually<Rect> {
(*self).render(b, a)
(*self)(b, a)
}
}

View file

@ -1,28 +1,77 @@
use crate::core::*;
use better_panic::{Settings, Verbosity};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen,
enable_raw_mode, disable_raw_mode
};
impl<T: Render + Handle + Send + Sized + 'static> Run for T {}
pub trait Run: Render + Handle + Send + Sized + 'static {
fn run (self, callback: Option<impl FnOnce(Arc<Mutex<Self>>)->Usually<()>>) -> Usually<()> {
let device = Arc::new(Mutex::new(self));
fn run (self, init: Option<impl FnOnce(Arc<Mutex<Self>>)->Usually<()>>) -> Usually<()> {
let app = Arc::new(Mutex::new(self));
let exited = Arc::new(AtomicBool::new(false));
let _input_thread = input_thread(&exited, &device);
let _input_thread = input_thread(&exited, &app);
terminal_setup()?;
panic_hook_setup();
let main_thread = main_thread(&exited, &device)?;
if let Some(callback) = callback {
callback(device);
let main_thread = main_thread(&exited, &app)?;
if let Some(init) = init {
init(app)?;
}
main_thread.join();
main_thread.join().unwrap();
terminal_teardown()?;
Ok(())
}
}
impl<T: Render + Handle + Send + Sized + 'static> Run for T {}
/// 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<AtomicBool>,
device: &Arc<Mutex<impl Render + Send + 'static>>
) -> Usually<JoinHandle<()>> {
let exited = exited.clone();
let device = device.clone();
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
let sleep = std::time::Duration::from_millis(16);
Ok(spawn(move || loop {
terminal.draw(|frame|{
let area = frame.size();
let buffer = frame.buffer_mut();
device.lock().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<AtomicBool>,
@ -54,52 +103,3 @@ pub fn input_thread (
}
})
}
/// Set up terminal
pub fn terminal_setup () -> Usually<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
Ok(())
}
/// Set up panic hook
pub fn panic_hook_setup () {
let better_panic_handler = better_panic::Settings::auto()
.verbosity(better_panic::Verbosity::Full)
.create_panic_handler();
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
stdout().execute(LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
better_panic_handler(info);
}));
}
/// Main thread render loop
pub fn main_thread (
exited: &Arc<AtomicBool>,
device: &Arc<Mutex<impl Render + Send + 'static>>
) -> Usually<JoinHandle<()>> {
let exited = exited.clone();
let device = device.clone();
let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?;
let sleep = std::time::Duration::from_millis(16);
Ok(spawn(move || loop {
terminal.draw(|frame|{
let area = frame.size();
let buffer = frame.buffer_mut();
device.lock().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);
}))
}
/// Cleanup
pub fn terminal_teardown () -> Usually<()> {
stdout().execute(LeaveAlternateScreen)?;
crossterm::terminal::disable_raw_mode()?;
Ok(())
}

View file

@ -1,15 +1,23 @@
use crate::core::*;
#[derive(Default)]
use atomic_float::AtomicF64;
#[derive(Debug)]
pub struct Timebase {
/// Frames per second
pub rate: ::atomic_float::AtomicF64,
pub rate: AtomicF64,
/// Beats per minute
pub bpm: ::atomic_float::AtomicF64,
pub bpm: AtomicF64,
/// Ticks per beat
pub ppq: ::atomic_float::AtomicF64,
pub ppq: AtomicF64,
}
impl Default for Timebase {
fn default () -> Self {
Self {
rate: 48000f64.into(),
bpm: 125f64.into(),
ppq: 96f64.into(),
}
}
}
impl Timebase {
pub fn new (rate: f64, bpm: f64, ppq: f64) -> Self {
Self { rate: rate.into(), bpm: bpm.into(), ppq: ppq.into() }
@ -146,7 +154,6 @@ impl Timebase {
ticks
}
}
#[cfg(test)] mod test {
use super::*;