From 2837ffff4abdc7052bfa9eafc8b93fd307e155f6 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sun, 30 Jun 2024 22:47:17 +0300 Subject: [PATCH] modularize core --- src/config.rs | 2 +- src/core/device.rs | 90 ++++++ src/core/handle.rs | 89 ++++++ src/core/jack.rs | 35 +++ src/core/keymap.rs | 39 +++ src/core/mod.rs | 121 ++++++++ src/{layout/collect.rs => core/note.rs} | 0 src/core/port.rs | 50 ++++ src/core/process.rs | 0 src/core/render.rs | 54 ++++ src/{ => core}/time.rs | 3 +- src/device.rs | 370 ------------------------ src/device/README.md | 21 -- src/device/chain/mod.rs | 9 +- src/device/chain/plugin.rs | 2 +- src/device/chain/plugin/lv2.rs | 2 +- src/device/chain/plugin/vst2.rs | 2 +- src/device/chain/sampler.rs | 2 +- src/device/launcher/grid.rs | 2 +- src/device/launcher/handle.rs | 6 +- src/device/launcher/mod.rs | 17 +- src/device/looper.rs | 2 +- src/device/mixer.rs | 3 +- src/device/mod.rs | 7 + src/device/sequencer/handle.rs | 9 +- src/device/sequencer/horizontal.rs | 2 +- src/device/sequencer/keys.rs | 2 +- src/device/sequencer/mod.rs | 97 ++----- src/device/sequencer/phrase.rs | 3 +- src/device/sequencer/process.rs | 7 +- src/device/sequencer/vertical.rs | 13 +- src/device/track.rs | 4 +- src/device/transport.rs | 37 ++- src/draw.rs | 111 ------- src/layout/container.rs | 3 +- src/layout/focus.rs | 3 +- src/layout/lozenge.rs | 2 +- src/layout/mod.rs | 25 +- src/layout/scroll.rs | 2 - src/layout/table.rs | 2 +- src/main.rs | 11 +- src/note.rs | 26 -- src/prelude.rs | 112 ------- 43 files changed, 629 insertions(+), 770 deletions(-) create mode 100644 src/core/device.rs create mode 100644 src/core/handle.rs create mode 100644 src/core/jack.rs create mode 100644 src/core/keymap.rs create mode 100644 src/core/mod.rs rename src/{layout/collect.rs => core/note.rs} (100%) create mode 100644 src/core/port.rs create mode 100644 src/core/process.rs create mode 100644 src/core/render.rs rename src/{ => core}/time.rs (99%) delete mode 100644 src/device.rs delete mode 100644 src/device/README.md create mode 100644 src/device/mod.rs delete mode 100644 src/draw.rs delete mode 100644 src/note.rs delete mode 100644 src/prelude.rs diff --git a/src/config.rs b/src/config.rs index 6d245117..543965af 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::core::*; const CONFIG_FILE_NAME: &'static str = "tek.toml"; const PROJECT_FILE_NAME: &'static str = "project.toml"; diff --git a/src/core/device.rs b/src/core/device.rs new file mode 100644 index 00000000..18f9d6ba --- /dev/null +++ b/src/core/device.rs @@ -0,0 +1,90 @@ +use crate::core::*; + +/// A UI component that may have presence on the JACK grap. +pub trait Device: Render + Handle + PortList + Send + Sync { + 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 {} + +/// A device dynamicammy composed of state and handlers. +pub struct DynamicDevice { + pub state: Arc>, + pub render: MutexUsually + Send>>, + pub handle: ArcUsually + Send>>>, + pub process: ArcControl + Send>>>, + pub client: Option +} + +impl Handle for DynamicDevice { + fn handle (&mut self, event: &AppEvent) -> Usually { + self.handle.lock().unwrap()(&mut *self.state.lock().unwrap(), event) + } +} + +impl Render for DynamicDevice { + fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { + self.render.lock().unwrap()(&*self.state.lock().unwrap(), buf, area) + } +} + +impl PortList for DynamicDevice { + fn audio_ins (&self) -> Usually> { + self.state().audio_ins() + } + fn audio_outs (&self) -> Usually> { + self.state().audio_outs() + } + fn midi_ins (&self) -> Usually> { + self.state().midi_ins() + } + fn midi_outs (&self) -> Usually> { + self.state().midi_outs() + } +} + +type DynamicAsyncClient = AsyncClient; +type DynamicNotifications = Notifications>; +type DynamicProcessHandler = ClosureProcessHandler; + +impl DynamicDevice { + pub fn new <'a, R, H, P> (render: R, handle: H, process: P, state: T) -> Self where + R: FnMut(&T, &mut Buffer, Rect) -> Usually + Send + 'static, + H: FnMut(&mut T, &AppEvent) -> Usually + Send + 'static, + P: FnMut(&mut T, &Client, &ProcessScope) -> Control + Send + 'static, + { + Self { + state: Arc::new(Mutex::new(state)), + render: Mutex::new(Box::new(render)), + handle: Arc::new(Mutex::new(Box::new(handle))), + process: Arc::new(Mutex::new(Box::new(process))), + client: None, + } + } + pub fn state (&self) -> std::sync::MutexGuard<'_, T> { + self.state.lock().unwrap() + } + pub fn activate (mut self, client: Client) -> Usually { + self.client = Some(client.activate_async(Notifications(Box::new({ + let state = self.state.clone(); + let handle = self.handle.clone(); + move|event|{ + let mut state = state.lock().unwrap(); + let mut handle = handle.lock().unwrap(); + handle(&mut state, &event).unwrap(); + } + }) as Box), ClosureProcessHandler::new(Box::new({ + let state = self.state.clone(); + let process = self.process.clone(); + move|client: &Client, scope: &ProcessScope|{ + let mut state = state.lock().unwrap(); + let mut process = process.lock().unwrap(); + (process)(&mut state, client, scope) + } + }) as BoxedProcessHandler))?); + Ok(self) + } +} diff --git a/src/core/handle.rs b/src/core/handle.rs new file mode 100644 index 00000000..f213256a --- /dev/null +++ b/src/core/handle.rs @@ -0,0 +1,89 @@ +use crate::core::*; + +/// Trait for things that handle input events. +pub trait Handle { + /// Handle an input event. + /// Returns Ok(true) if the device handled the event. + /// This is the mechanism which allows nesting of components;. + fn handle (&mut self, _e: &AppEvent) -> Usually { + Ok(false) + } +} + +#[derive(Debug)] +pub enum AppEvent { + /// 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) +} + +#[derive(Debug)] +pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, String), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(String, bool), + PortRegistration(PortId, bool), + PortRename(PortId, String, String), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +pub struct Notifications(pub T); + +impl NotificationHandler for Notifications { + fn thread_init (&self, _: &Client) { + self.0(AppEvent::Jack(JackEvent::ThreadInit)); + } + + fn shutdown (&mut self, status: ClientStatus, reason: &str) { + self.0(AppEvent::Jack(JackEvent::Shutdown(status, reason.into()))); + } + + fn freewheel (&mut self, _: &Client, enabled: bool) { + self.0(AppEvent::Jack(JackEvent::Freewheel(enabled))); + } + + fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { + self.0(AppEvent::Jack(JackEvent::SampleRate(frames))); + Control::Quit + } + + fn client_registration (&mut self, _: &Client, name: &str, reg: bool) { + self.0(AppEvent::Jack(JackEvent::ClientRegistration(name.into(), reg))); + } + + fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) { + self.0(AppEvent::Jack(JackEvent::PortRegistration(id, reg))); + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(AppEvent::Jack(JackEvent::PortRename(id, old.into(), new.into()))); + Control::Continue + } + + fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(AppEvent::Jack(JackEvent::PortsConnected(a, b, are))); + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + self.0(AppEvent::Jack(JackEvent::GraphReorder)); + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + self.0(AppEvent::Jack(JackEvent::XRun)); + Control::Continue + } +} diff --git a/src/core/jack.rs b/src/core/jack.rs new file mode 100644 index 00000000..4e877f66 --- /dev/null +++ b/src/core/jack.rs @@ -0,0 +1,35 @@ +use crate::core::*; + +pub type BoxedNotificationHandler = + Box; + +pub type BoxedProcessHandler = + Box Control + Send>; + +pub type Jack = + AsyncClient>; + +pub use ::jack::{ + AsyncClient, + AudioIn, + AudioOut, + Client, + ClientOptions, + ClientStatus, + ClosureProcessHandler, + Control, + Frames, + MidiIn, + MidiOut, + NotificationHandler, + Port, + PortFlags, + PortId, + PortSpec, + ProcessHandler, + ProcessScope, + RawMidi, + Transport, + TransportState, + TransportStatePosition +}; diff --git a/src/core/keymap.rs b/src/core/keymap.rs new file mode 100644 index 00000000..d9ea4919 --- /dev/null +++ b/src/core/keymap.rs @@ -0,0 +1,39 @@ +use crate::core::*; + +pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; + +pub type KeyBinding = ( + KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler +); + +pub type KeyMap = [KeyBinding]; + +pub fn handle_keymap ( + state: &mut T, event: &AppEvent, keymap: &KeyMap, +) -> Usually { + match event { + AppEvent::Input(Event::Key(event)) => { + for (code, modifiers, _, _, command) in keymap.iter() { + if *code == event.code && modifiers.bits() == event.modifiers.bits() { + return command(state) + } + } + }, + _ => {} + }; + Ok(false) +} + +#[macro_export] macro_rules! key { + ($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => { + (KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f) + }; +} + +#[macro_export] macro_rules! keymap { + ($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => { + &[ + $((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),* + ] as &'static [KeyBinding<$T>] + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 00000000..b55650b9 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,121 @@ +pub type Usually = Result>; + +macro_rules! submod { + ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; +} + +submod!( device handle jack keymap note port process render time ); + +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, + atomic::{ + Ordering, + AtomicBool, + AtomicUsize + }, + mpsc::{self, channel, Sender, Receiver} +}; + +pub use ::crossterm::{ + ExecutableCommand, QueueableCommand, + event::{Event, KeyEvent, KeyCode, KeyModifiers}, + terminal::{ + self, + Clear, ClearType, + EnterAlternateScreen, LeaveAlternateScreen, + enable_raw_mode, disable_raw_mode + }, +}; + +pub use ::ratatui::{ + prelude::{ + Buffer, Rect, Style, Color, CrosstermBackend, Layout, Stylize, Direction, + Line, Constraint + }, + widgets::{Widget, WidgetRef}, + //style::Stylize, +}; + +pub use ::midly::{ + MidiMessage, + live::LiveEvent, + num::u7 +}; + +pub use crate::{key, keymap}; + +/// Run a device as the root of the app. +pub fn run (device: impl Device + Send + Sync + 'static) -> Result<(), Box> { + let device = Arc::new(Mutex::new(device)); + let exited = Arc::new(AtomicBool::new(false)); + + // Spawn input (handle) thread + let _input_thread = { + let poll = std::time::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.lock().unwrap().handle(&AppEvent::Input(event)).is_err() { + break + } + } + } + }) + }; + + // Set up terminal + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; + + // Set up panic hook + 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 (render) loop + let sleep = std::time::Duration::from_millis(16); + 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 + stdout().execute(LeaveAlternateScreen)?; + crossterm::terminal::disable_raw_mode()?; + + Ok(()) +} diff --git a/src/layout/collect.rs b/src/core/note.rs similarity index 100% rename from src/layout/collect.rs rename to src/core/note.rs diff --git a/src/core/port.rs b/src/core/port.rs new file mode 100644 index 00000000..ee6b7243 --- /dev/null +++ b/src/core/port.rs @@ -0,0 +1,50 @@ +use crate::core::*; + +/// Trait for things that may expose JACK ports. +pub trait PortList { + fn audio_ins (&self) -> Usually> { + Ok(vec![]) + } + fn audio_outs (&self) -> Usually> { + Ok(vec![]) + } + fn midi_ins (&self) -> Usually> { + Ok(vec![]) + } + fn midi_outs (&self) -> Usually> { + Ok(vec![]) + } + fn connect (&mut self, _connect: bool, _source: &str, _target: &str) + -> Usually<()> + { + Ok(()) + } + fn connect_all (&mut self, connections: &[(bool, &str, &str)]) + -> Usually<()> + { + for (connect, source, target) in connections.iter() { + self.connect(*connect, source, target)?; + } + Ok(()) + } +} + +pub struct DevicePort { + pub name: String, + pub port: Port, + pub connect: Vec, +} + +impl DevicePort { + pub fn new (client: &Client, name: &str, connect: &[&str]) -> Usually { + let mut connects = vec![]; + for port in connect.iter() { + connects.push(port.to_string()); + } + Ok(Self { + name: name.to_string(), + port: client.register_port(name, T::default())?, + connect: connects, + }) + } +} diff --git a/src/core/process.rs b/src/core/process.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/core/render.rs b/src/core/render.rs new file mode 100644 index 00000000..5560a6d3 --- /dev/null +++ b/src/core/render.rs @@ -0,0 +1,54 @@ +use crate::core::*; + +/// Trait for things that render to the display. +pub trait Render { + // 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 { + 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 + } +} + +impl Render for Box { + fn render (&self, b: &mut Buffer, a: Rect) -> Usually { + (**self).render(b, a) + } +} + +impl WidgetRef for &dyn Render { + fn render_ref (&self, area: Rect, buf: &mut Buffer) { + Render::render(*self, buf, area).expect("Failed to render device."); + } +} + +impl WidgetRef for dyn Render { + fn render_ref (&self, area: Rect, buf: &mut Buffer) { + Render::render(self, buf, area).expect("Failed to render device."); + } +} + +pub trait Blit { + // Render something to X, Y coordinates in a buffer, ignoring width/height. + fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option