1use std::ops::Range;
21use std::path::Path;
22use std::sync::{Arc, OnceLock};
23
24use dspk_core::{DspResult, RecordingSource};
25use dspk_io::SourceEntry;
26use gpui_kit::{AnyElement, AnyView, App, Entity, Window};
27
28use crate::data::store::Store;
29use crate::domain::session::ViewDoc;
30use crate::navigation::route::Workspace;
31use crate::ui::plot::canvas::Pixel;
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct Layers {
37 pub spikes: bool,
38 pub units: bool,
39}
40
41impl Layers {
42 fn or(self, other: Layers) -> Layers {
43 Layers { spikes: self.spikes || other.spikes, units: self.units || other.units }
44 }
45}
46
47pub fn layers(store: &Store, view: &ViewDoc, cx: &App) -> Layers {
49 plugins().iter().fold(Layers::default(), |acc, p| acc.or(p.layers(store, view, cx)))
50}
51
52#[derive(Debug, Clone, PartialEq, Default)]
54pub struct ChannelLayout {
55 pub group_kind: String,
57 pub groups: Vec<ChannelGroup>,
58 pub labels: Vec<Option<String>>,
60}
61
62#[derive(Debug, Clone, PartialEq)]
63pub struct ChannelGroup {
64 pub name: String,
65 pub channels: Vec<usize>,
67}
68
69impl ChannelLayout {
70 pub fn label(&self, channel: usize) -> Option<&str> {
71 self.labels.get(channel)?.as_deref()
72 }
73}
74
75pub trait SourceOpener: Send + Sync {
77 fn reads(&self, path: &Path) -> bool;
79 fn sources(&self, path: &Path) -> DspResult<Vec<SourceEntry>>;
80 fn open(&self, path: &Path, id: &str) -> DspResult<Box<dyn RecordingSource>>;
81 fn layout(&self, _path: &Path, _id: &str) -> Option<ChannelLayout> {
83 None
84 }
85}
86
87#[derive(Debug, Clone, Default)]
91pub struct TraceOverlay {
92 pub dim: bool,
93 pub units: Vec<OverlayUnit>,
94 pub marks: Vec<OverlayMark>,
95}
96
97#[derive(Debug, Clone)]
98pub struct OverlayUnit {
99 pub color: Pixel,
100 pub channels: Vec<usize>,
102}
103
104#[derive(Debug, Clone, Copy)]
105pub struct OverlayMark {
106 pub start_sec: f64,
108 pub end_sec: f64,
109 pub unit: u32,
111}
112
113#[derive(Debug, Clone, Default)]
116pub struct Raster {
117 pub rows: Vec<RasterRow>,
118}
119
120#[derive(Debug, Clone)]
121pub struct RasterRow {
122 pub color: Pixel,
123 pub label: String,
124 pub times: Vec<f64>,
125}
126
127pub trait AppPlugin: Send + Sync + 'static {
129 fn id(&self) -> &'static str;
131 fn name(&self) -> &'static str;
132
133 fn opener(&self) -> Option<&dyn SourceOpener> {
134 None
135 }
136
137 fn synthetic(&self, _channels: usize, _sample_rate: f64, _duration_sec: f64) -> Option<anyhow::Result<Arc<dyn RecordingSource>>> {
139 None
140 }
141
142 fn install(&self, _store: &Entity<Store>, _cx: &mut App) {}
144
145 fn trace_overlay(&self, _store: &Store, _view: &ViewDoc, _window_sec: Range<f64>, _cx: &App) -> Option<TraceOverlay> {
147 None
148 }
149
150 fn raster(&self, _store: &Store, _view: &ViewDoc, _window_sec: Range<f64>, _cx: &App) -> Option<Raster> {
152 None
153 }
154
155 fn layers(&self, _store: &Store, _view: &ViewDoc, _cx: &App) -> Layers {
157 Layers::default()
158 }
159
160 fn recording_epochs(&self, _path: &Path, _start_sec: f64) -> Vec<crate::domain::epoch::EpochDoc> {
163 Vec::new()
164 }
165
166 fn aligned(&self, _store: &Entity<Store>, _events_s: &[f64], _pre_ms: f64, _post_ms: f64, _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
169 None
170 }
171
172 fn workspace(&self, _ws: Workspace, _store: &Entity<Store>, _window: &mut Window, _cx: &mut App) -> Option<AnyView> {
174 None
175 }
176
177 fn provides(&self, _ws: Workspace) -> bool {
179 false
180 }
181
182 fn panel(&self) -> Option<(&'static str, std::rc::Rc<dyn Fn() -> gpui_kit::component::Icon>)> {
184 None
185 }
186
187 fn view_section(&self, _store: &Entity<Store>, _view: &ViewDoc, _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
189 None
190 }
191}
192
193static PLUGINS: OnceLock<Vec<Arc<dyn AppPlugin>>> = OnceLock::new();
194
195pub fn register(plugins: Vec<Arc<dyn AppPlugin>>) {
197 if PLUGINS.set(plugins).is_err() {
198 tracing::warn!("plugins are registered once");
199 }
200}
201
202pub fn plugins() -> &'static [Arc<dyn AppPlugin>] {
204 PLUGINS.get().map_or(&[], Vec::as_slice)
205}
206
207pub fn sources(path: &Path) -> DspResult<Vec<SourceEntry>> {
209 match opener_of(path) {
210 Some(o) => o.sources(path),
211 None => dspk_io::sources(path),
212 }
213}
214
215fn opener_of(path: &Path) -> Option<&'static dyn SourceOpener> {
217 plugins().iter().filter_map(|p| p.opener()).find(|o| o.reads(path))
218}
219
220pub fn open_source(path: &Path, id: &str) -> DspResult<Box<dyn RecordingSource>> {
222 match opener_of(path) {
223 Some(o) => o.open(path, id),
224 None => dspk_io::open_source(path, id),
225 }
226}
227
228pub fn open(path: &Path, id: Option<&str>) -> DspResult<Box<dyn RecordingSource>> {
230 match id {
231 Some(id) => open_source(path, id),
232 None => {
233 let list = sources(path)?;
234 let id = dspk_io::default_source(&list).map(|e| e.id.clone()).unwrap_or_default();
235 open_source(path, &id)
236 }
237 }
238}
239
240pub fn layout(path: &Path, id: &str) -> Option<ChannelLayout> {
242 opener_of(path)?.layout(path, id)
243}
244
245pub fn synthetic(channels: usize, sample_rate: f64, duration_sec: f64) -> Option<anyhow::Result<Arc<dyn RecordingSource>>> {
247 plugins().iter().find_map(|p| p.synthetic(channels, sample_rate, duration_sec))
248}