Skip to main content

trem_tui/
app.rs

1//! Main TUI application: grid, views, transport, and [`trem_cpal::Bridge`] integration.
2//!
3//! [`App::run`] is the event loop (draw, input, non-blocking audio poll).
4
5use crate::input::{self, Action, BottomPane, Editor, InputContext, Mode};
6use crate::project::ProjectData;
7use crate::view::graph::GraphViewWidget;
8use crate::view::help::HelpOverlay;
9use crate::view::info::InfoView;
10use crate::view::pattern::PatternView;
11use crate::view::perf::HostStatsSnapshot;
12use crate::view::scope::ScopeView;
13use crate::view::spectrum::{SpectrumAnalyzerState, SpectrumView};
14use crate::view::transport::TransportView;
15
16use trem::event::NoteEvent;
17use trem::graph::{Edge, GraphSnapshot, ParamDescriptor};
18use trem::math::Rational;
19use trem::pitch::Pitch;
20use trem_cpal::{Bridge, Command, Notification, ScopeFocus};
21
22use crossterm::event::{self, Event, KeyEventKind};
23use ratatui::layout::{Constraint, Direction, Layout};
24use std::collections::{HashMap, HashSet};
25use std::time::{Duration, Instant};
26
27use sysinfo::{Pid, ProcessesToUpdate, System};
28
29const GATE_PRESETS: [(i64, u64); 4] = [(1, 4), (1, 2), (3, 4), (7, 8)];
30
31/// Minimum width for the pattern/graph pane; sidebar shrinks first.
32const MAIN_EDITOR_MIN_WIDTH: u16 = 14;
33
34/// Sidebar width (cursor/project/keys + perf stacked) from `outer[1].width`.
35fn info_sidebar_width(middle_width: u16) -> u16 {
36    const MIN_SIDEBAR: u16 = 18;
37    let w = middle_width.max(MAIN_EDITOR_MIN_WIDTH + MIN_SIDEBAR);
38    let target = (u32::from(w) * 36 / 100).clamp(u32::from(MIN_SIDEBAR), 30) as u16;
39    target.min(w.saturating_sub(MAIN_EDITOR_MIN_WIDTH))
40}
41
42fn cycle_gate(current: Rational) -> Rational {
43    for (i, &(n, d)) in GATE_PRESETS.iter().enumerate() {
44        if current == Rational::new(n, d) {
45            let next = GATE_PRESETS[(i + 1) % GATE_PRESETS.len()];
46            return Rational::new(next.0, next.1);
47        }
48    }
49    Rational::new(1, 4)
50}
51
52/// Mutable state for the full terminal UI: pattern/graph views, audio bridge, and layout data.
53pub struct App {
54    pub grid: trem::grid::Grid,
55    pub cursor_row: u32,
56    pub cursor_col: u32,
57    pub mode: Mode,
58    pub editor: Editor,
59    /// Full keymap overlay (`?` / Esc).
60    pub help_open: bool,
61    pub bpm: f64,
62    pub playing: bool,
63    /// After the first **Play**, pattern edits sync to the audio thread even while **paused**
64    /// (playhead is held until **Play** again).
65    engine_pattern_active: bool,
66    pub beat_position: f64,
67    pub current_play_row: Option<u32>,
68    pub scale: trem::pitch::Scale,
69    pub scale_name: String,
70    pub octave: i32,
71    pub bridge: Bridge,
72    /// Master output (post–FX), interleaved stereo — waveform / spectrum.
73    pub scope_master: Vec<f32>,
74    /// Instrument submix (pre–master bus), same layout — graph view **IN** preview.
75    pub scope_graph_in: Vec<f32>,
76    /// This-process CPU / RSS refreshed ~2× per second for the info panel.
77    pub host_stats: HostStatsSnapshot,
78    sys: System,
79    host_stats_last_refresh: Instant,
80    /// Peak-decay time constant for spectrum bars (ms); lower = snappier, higher = longer “tail”.
81    pub spectrum_fall_ms: f64,
82    spectrum_analyzer_in: SpectrumAnalyzerState,
83    spectrum_analyzer_out: SpectrumAnalyzerState,
84    pub peak_l: f32,
85    pub peak_r: f32,
86    pub should_quit: bool,
87    pub instrument_names: Vec<String>,
88    pub voice_ids: Vec<u32>,
89    pub graph_nodes: Vec<(u32, String)>,
90    pub graph_node_descriptions: Vec<String>,
91    pub graph_edges: Vec<Edge>,
92    pub graph_cursor: usize,
93    pub graph_depths: Vec<usize>,
94    pub graph_layers: Vec<Vec<usize>>,
95    pub graph_params: Vec<Vec<ParamDescriptor>>,
96    pub graph_param_values: Vec<Vec<f64>>,
97    pub graph_param_groups: Vec<Vec<trem::graph::ParamGroup>>,
98    pub param_cursor: usize,
99    pub swing: f64,
100    pub euclidean_k: u32,
101    pub undo_stack: Vec<Vec<Option<NoteEvent>>>,
102    pub redo_stack: Vec<Vec<Option<NoteEvent>>>,
103    rng_state: u64,
104    preview_note_off: Option<(u32, Instant)>,
105    pub bottom_pane: BottomPane,
106    /// Path into nested graphs for the graph editor (empty = root).
107    pub graph_path: Vec<u32>,
108    /// Stack of saved cursor positions when diving into nested graphs.
109    pub graph_stack: Vec<GraphFrame>,
110    /// Tracks which nodes have inner graphs for visual indicators.
111    pub graph_has_children: Vec<bool>,
112    /// Breadcrumb labels for the navigation path.
113    pub graph_breadcrumb: Vec<String>,
114    /// Pregenerated inner-graph snapshots from the host (`Graph::nested_ui_snapshots`), keyed by
115    /// the same path the UI uses after [`Self::enter_nested_graph`] (e.g. `[lead_id]`).
116    nested_graph_snapshots: HashMap<Vec<u32>, GraphSnapshot>,
117}
118
119/// Saved state when diving into a nested graph node.
120#[derive(Clone, Debug)]
121pub struct GraphFrame {
122    pub nodes: Vec<(u32, String)>,
123    pub edges: Vec<Edge>,
124    pub cursor: usize,
125    pub params: Vec<Vec<ParamDescriptor>>,
126    pub param_values: Vec<Vec<f64>>,
127    pub param_groups: Vec<Vec<trem::graph::ParamGroup>>,
128    pub depths: Vec<usize>,
129    pub layers: Vec<Vec<usize>>,
130    pub has_children: Vec<bool>,
131    pub node_descriptions: Vec<String>,
132}
133
134impl App {
135    /// Initial pattern view, scale metadata, and per-column voice IDs for [`trem_cpal::Command::NoteOn`].
136    pub fn new(
137        grid: trem::grid::Grid,
138        scale: trem::pitch::Scale,
139        scale_name: String,
140        bridge: Bridge,
141        instrument_names: Vec<String>,
142        voice_ids: Vec<u32>,
143    ) -> Self {
144        Self {
145            grid,
146            cursor_row: 0,
147            cursor_col: 0,
148            mode: Mode::Normal,
149            editor: Editor::Pattern,
150            help_open: false,
151            bpm: 120.0,
152            playing: false,
153            engine_pattern_active: false,
154            beat_position: 0.0,
155            current_play_row: None,
156            scale,
157            scale_name,
158            octave: 0,
159            bridge,
160            scope_master: Vec::new(),
161            scope_graph_in: Vec::new(),
162            host_stats: HostStatsSnapshot::default(),
163            sys: System::new(),
164            host_stats_last_refresh: Instant::now()
165                .checked_sub(Duration::from_secs(1))
166                .unwrap_or_else(Instant::now),
167            spectrum_fall_ms: 18.0,
168            spectrum_analyzer_in: SpectrumAnalyzerState::new(18.0),
169            spectrum_analyzer_out: SpectrumAnalyzerState::new(18.0),
170            peak_l: 0.0,
171            peak_r: 0.0,
172            should_quit: false,
173            instrument_names,
174            voice_ids,
175            graph_nodes: Vec::new(),
176            graph_node_descriptions: Vec::new(),
177            graph_edges: Vec::new(),
178            graph_cursor: 0,
179            graph_depths: Vec::new(),
180            graph_layers: Vec::new(),
181            graph_params: Vec::new(),
182            graph_param_values: Vec::new(),
183            graph_param_groups: Vec::new(),
184            param_cursor: 0,
185            swing: 0.0,
186            euclidean_k: 0,
187            undo_stack: Vec::new(),
188            redo_stack: Vec::new(),
189            rng_state: std::time::SystemTime::now()
190                .duration_since(std::time::UNIX_EPOCH)
191                .unwrap_or_default()
192                .as_nanos() as u64,
193            preview_note_off: None,
194            bottom_pane: BottomPane::Spectrum,
195            graph_path: Vec::new(),
196            graph_stack: Vec::new(),
197            graph_has_children: Vec::new(),
198            graph_breadcrumb: Vec::new(),
199            nested_graph_snapshots: HashMap::new(),
200        }
201    }
202
203    /// Attaches node/edge/param snapshots for the graph editor (from the host graph).
204    pub fn with_graph_info(
205        mut self,
206        nodes: Vec<(u32, String)>,
207        edges: Vec<Edge>,
208        params: Vec<(Vec<ParamDescriptor>, Vec<f64>, Vec<trem::graph::ParamGroup>)>,
209    ) -> Self {
210        let (depths, layers) = crate::view::graph::compute_graph_nav(&nodes, &edges);
211        self.graph_nodes = nodes;
212        self.graph_edges = edges;
213        self.graph_depths = depths;
214        self.graph_layers = layers;
215        self.graph_params = params.iter().map(|(d, _, _)| d.clone()).collect();
216        self.graph_param_values = params.iter().map(|(_, v, _)| v.clone()).collect();
217        self.graph_param_groups = params.into_iter().map(|(_, _, g)| g).collect();
218        self.graph_has_children = vec![false; self.graph_nodes.len()];
219        self
220    }
221
222    /// Sets processor descriptions for each node (shown in info help).
223    pub fn set_node_descriptions(&mut self, descriptions: Vec<String>) {
224        self.graph_node_descriptions = descriptions;
225    }
226
227    /// Marks which nodes have inner (nested) graphs for the graph view indicator.
228    pub fn set_node_children(&mut self, has_children: Vec<bool>) {
229        self.graph_has_children = has_children;
230    }
231
232    /// Supplies snapshots for nested graph levels so **Graph › Enter** shows nodes and parameters.
233    pub fn with_nested_graph_snapshots(
234        mut self,
235        snapshots: HashMap<Vec<u32>, GraphSnapshot>,
236    ) -> Self {
237        self.nested_graph_snapshots = snapshots;
238        self
239    }
240
241    fn refresh_host_stats(&mut self) {
242        if self.host_stats_last_refresh.elapsed() < Duration::from_millis(520) {
243            return;
244        }
245        self.host_stats_last_refresh = Instant::now();
246        self.sys.refresh_cpu_usage();
247        let pid = Pid::from_u32(std::process::id());
248        self.sys
249            .refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
250        if let Some(p) = self.sys.process(pid) {
251            // Smoothed: raw `cpu_usage` is per refresh window and can spike (UI redraw + audio).
252            let raw = p.cpu_usage();
253            let prev = self.host_stats.process_cpu_pct;
254            const SMOOTH: f32 = 0.22;
255            self.host_stats.process_cpu_pct = if prev <= f32::EPSILON {
256                raw
257            } else {
258                prev * (1.0 - SMOOTH) + raw * SMOOTH
259            };
260            self.host_stats.process_rss_mb = p.memory() / 1024 / 1024;
261        } else {
262            self.host_stats.process_cpu_pct = 0.0;
263            self.host_stats.process_rss_mb = 0;
264        }
265    }
266
267    /// Rebuilds graph editor state from a host [`GraphSnapshot`] (nodes, edges, params, layout).
268    fn load_graph_from_snapshot(&mut self, snap: &GraphSnapshot) {
269        let nodes: Vec<(u32, String)> = snap.nodes.iter().map(|n| (n.id, n.name.clone())).collect();
270        let edges = snap.edges.clone();
271        let (depths, layers) = crate::view::graph::compute_graph_nav(&nodes, &edges);
272        self.graph_nodes = nodes;
273        self.graph_edges = edges;
274        self.graph_depths = depths;
275        self.graph_layers = layers;
276        self.graph_params = snap.nodes.iter().map(|n| n.params.clone()).collect();
277        self.graph_param_values = snap.nodes.iter().map(|n| n.param_values.clone()).collect();
278        self.graph_param_groups = snap.nodes.iter().map(|n| n.param_groups.clone()).collect();
279        self.graph_has_children = snap.nodes.iter().map(|n| n.has_children).collect();
280        self.graph_node_descriptions = vec![String::new(); self.graph_nodes.len()];
281        self.graph_cursor = 0;
282        self.param_cursor = 0;
283    }
284
285    /// Tells the audio thread which signal to show in the bottom **IN | OUT** previews.
286    pub fn sync_scope_focus(&mut self) {
287        match self.editor {
288            Editor::Pattern => {
289                self.bridge
290                    .send(Command::SetScopeFocus(ScopeFocus::PatchBuses));
291            }
292            Editor::Graph => {
293                if let Some(&(nid, _)) = self.graph_nodes.get(self.graph_cursor) {
294                    self.bridge
295                        .send(Command::SetScopeFocus(ScopeFocus::GraphNode {
296                            graph_path: self.graph_path.clone(),
297                            node: nid,
298                        }));
299                } else {
300                    self.bridge
301                        .send(Command::SetScopeFocus(ScopeFocus::PatchBuses));
302                }
303            }
304        }
305    }
306
307    /// Applies one [`Action`] from input: updates state and sends [`Command`]s to the audio bridge as needed.
308    pub fn handle_action(&mut self, action: Action) {
309        let sync_scope = matches!(
310            &action,
311            Action::CycleEditor
312                | Action::EnterGraph
313                | Action::ExitGraph
314                | Action::MoveUp
315                | Action::MoveDown
316                | Action::MoveLeft
317                | Action::MoveRight
318                | Action::LoadProject
319        );
320        match action {
321            Action::Quit => self.should_quit = true,
322            Action::ToggleHelp => {
323                self.help_open = !self.help_open;
324                if self.help_open {
325                    self.mode = Mode::Normal;
326                }
327            }
328            Action::CycleEditor => {
329                self.editor = self.editor.next();
330                self.mode = Mode::Normal;
331            }
332            Action::ToggleEdit => {
333                self.mode = match self.mode {
334                    Mode::Normal => {
335                        self.param_cursor = 0;
336                        Mode::Edit
337                    }
338                    Mode::Edit => Mode::Normal,
339                };
340            }
341            Action::TogglePlay => {
342                self.playing = !self.playing;
343                if self.playing {
344                    self.engine_pattern_active = true;
345                    self.send_pattern();
346                    self.bridge.send(Command::Play);
347                } else {
348                    self.bridge.send(Command::Pause);
349                }
350            }
351            Action::MoveUp => match (&self.editor, &self.mode) {
352                (Editor::Pattern, _) => {
353                    self.cursor_col = self.cursor_col.saturating_sub(1);
354                }
355                (Editor::Graph, Mode::Normal) => self.graph_move_up(),
356                (Editor::Graph, Mode::Edit) => {
357                    self.param_cursor = self.param_cursor.saturating_sub(1);
358                }
359            },
360            Action::MoveDown => match (&self.editor, &self.mode) {
361                (Editor::Pattern, _) => {
362                    if self.cursor_col < self.grid.columns.saturating_sub(1) {
363                        self.cursor_col += 1;
364                    }
365                }
366                (Editor::Graph, Mode::Normal) => self.graph_move_down(),
367                (Editor::Graph, Mode::Edit) => {
368                    let max = self.current_node_param_count().saturating_sub(1);
369                    if self.param_cursor < max {
370                        self.param_cursor += 1;
371                    }
372                }
373            },
374            Action::MoveLeft => match (&self.editor, &self.mode) {
375                (Editor::Pattern, _) => {
376                    self.cursor_row = self.cursor_row.saturating_sub(1);
377                }
378                (Editor::Graph, Mode::Normal) => self.graph_move_left(),
379                (Editor::Graph, Mode::Edit) => self.adjust_param_coarse(-1),
380            },
381            Action::MoveRight => match (&self.editor, &self.mode) {
382                (Editor::Pattern, _) => {
383                    if self.cursor_row < self.grid.rows.saturating_sub(1) {
384                        self.cursor_row += 1;
385                    }
386                }
387                (Editor::Graph, Mode::Normal) => self.graph_move_right(),
388                (Editor::Graph, Mode::Edit) => self.adjust_param_coarse(1),
389            },
390            Action::Undo => self.undo(),
391            Action::Redo => self.redo(),
392            Action::SaveProject => {
393                let path = crate::project::default_project_path();
394                let data = ProjectData::from_app(self);
395                if let Err(e) = crate::project::save(&path, &data) {
396                    eprintln!("save error: {e}");
397                }
398            }
399            Action::LoadProject => {
400                let path = crate::project::default_project_path();
401                match crate::project::load(&path) {
402                    Ok(data) => {
403                        self.push_undo();
404                        data.apply_to_app(self);
405                        if self.should_sync_pattern() {
406                            self.send_pattern();
407                        }
408                    }
409                    Err(e) => eprintln!("load error: {e}"),
410                }
411            }
412            Action::SwingUp => {
413                self.swing = (self.swing + 0.05).min(0.9);
414                if self.should_sync_pattern() {
415                    self.send_pattern();
416                }
417            }
418            Action::SwingDown => {
419                self.swing = (self.swing - 0.05).max(0.0);
420                if self.should_sync_pattern() {
421                    self.send_pattern();
422                }
423            }
424            Action::GateCycle => {
425                if self.editor == Editor::Pattern {
426                    if let Some(note) = self.grid.get(self.cursor_row, self.cursor_col).cloned() {
427                        self.push_undo();
428                        let new_gate = cycle_gate(note.gate);
429                        let mut updated = note;
430                        updated.gate = new_gate;
431                        self.grid
432                            .set(self.cursor_row, self.cursor_col, Some(updated));
433                        if self.should_sync_pattern() {
434                            self.send_pattern();
435                        }
436                    }
437                }
438            }
439            Action::NoteInput(degree) => {
440                if self.editor != Editor::Pattern {
441                    return;
442                }
443                self.push_undo();
444                let event = NoteEvent::new(degree, self.octave, Rational::new(3, 4));
445                let voice_id = self
446                    .voice_ids
447                    .get(self.cursor_col as usize)
448                    .copied()
449                    .unwrap_or(0);
450
451                if let Some((old_voice, _)) = self.preview_note_off.take() {
452                    self.bridge.send(Command::NoteOff { voice: old_voice });
453                }
454
455                let pitch = self.scale.resolve(degree);
456                let freq = Pitch(pitch.0 + self.octave as f64).to_hz(440.0);
457                let vel = event.velocity.to_f64();
458                self.bridge.send(Command::NoteOn {
459                    frequency: freq,
460                    velocity: vel,
461                    voice: voice_id,
462                });
463                self.preview_note_off = Some((voice_id, Instant::now()));
464
465                self.grid.set(self.cursor_row, self.cursor_col, Some(event));
466
467                if self.should_sync_pattern() {
468                    self.send_pattern();
469                }
470
471                if self.cursor_row < self.grid.rows.saturating_sub(1) {
472                    self.cursor_row += 1;
473                } else {
474                    self.cursor_row = 0;
475                    if self.cursor_col < self.grid.columns.saturating_sub(1) {
476                        self.cursor_col += 1;
477                    }
478                }
479            }
480            Action::DeleteNote => {
481                if self.editor != Editor::Pattern {
482                    return;
483                }
484                self.push_undo();
485                self.grid.set(self.cursor_row, self.cursor_col, None);
486                if self.should_sync_pattern() {
487                    self.send_pattern();
488                }
489            }
490            Action::OctaveUp => {
491                self.octave = (self.octave + 1).min(9);
492                if self.should_sync_pattern() {
493                    self.send_pattern();
494                }
495            }
496            Action::OctaveDown => {
497                self.octave = (self.octave - 1).max(-4);
498                if self.should_sync_pattern() {
499                    self.send_pattern();
500                }
501            }
502            Action::BpmUp => {
503                if self.editor == Editor::Graph && self.mode == Mode::Edit {
504                    self.adjust_param_fine(1);
505                } else {
506                    self.bpm = (self.bpm + 1.0).min(300.0);
507                    self.bridge.send(Command::SetBpm(self.bpm));
508                }
509            }
510            Action::BpmDown => {
511                if self.editor == Editor::Graph && self.mode == Mode::Edit {
512                    self.adjust_param_fine(-1);
513                } else {
514                    self.bpm = (self.bpm - 1.0).max(20.0);
515                    self.bridge.send(Command::SetBpm(self.bpm));
516                }
517            }
518            Action::ParamFineUp => {
519                if self.editor == Editor::Graph && self.mode == Mode::Edit {
520                    self.adjust_param_fine(1);
521                }
522            }
523            Action::ParamFineDown => {
524                if self.editor == Editor::Graph && self.mode == Mode::Edit {
525                    self.adjust_param_fine(-1);
526                }
527            }
528            Action::EuclideanFill => {
529                if self.editor == Editor::Pattern {
530                    self.push_undo();
531                    self.euclidean_k = (self.euclidean_k + 1) % (self.grid.rows + 1);
532                    let pattern = trem::euclidean::euclidean(self.euclidean_k, self.grid.rows);
533                    let template = NoteEvent::new(0, self.octave, Rational::new(3, 4));
534                    self.grid
535                        .fill_euclidean(self.cursor_col, &pattern, template);
536                    if self.should_sync_pattern() {
537                        self.send_pattern();
538                    }
539                }
540            }
541            Action::RandomizeVoice => {
542                if self.editor == Editor::Pattern {
543                    self.push_undo();
544                    self.randomize_current_voice();
545                    if self.should_sync_pattern() {
546                        self.send_pattern();
547                    }
548                }
549            }
550            Action::ReverseVoice => {
551                if self.editor == Editor::Pattern {
552                    self.push_undo();
553                    self.grid.reverse_voice(self.cursor_col);
554                    if self.should_sync_pattern() {
555                        self.send_pattern();
556                    }
557                }
558            }
559            Action::ShiftVoiceLeft => {
560                if self.editor == Editor::Pattern {
561                    self.push_undo();
562                    self.grid.shift_voice(self.cursor_col, -1);
563                    if self.should_sync_pattern() {
564                        self.send_pattern();
565                    }
566                }
567            }
568            Action::ShiftVoiceRight => {
569                if self.editor == Editor::Pattern {
570                    self.push_undo();
571                    self.grid.shift_voice(self.cursor_col, 1);
572                    if self.should_sync_pattern() {
573                        self.send_pattern();
574                    }
575                }
576            }
577            Action::VelocityUp => {
578                if self.editor == Editor::Pattern {
579                    self.push_undo();
580                    self.adjust_note_velocity(Rational::new(1, 8));
581                    if self.should_sync_pattern() {
582                        self.send_pattern();
583                    }
584                }
585            }
586            Action::VelocityDown => {
587                if self.editor == Editor::Pattern {
588                    self.push_undo();
589                    self.adjust_note_velocity(Rational::new(-1, 8));
590                    if self.should_sync_pattern() {
591                        self.send_pattern();
592                    }
593                }
594            }
595            Action::CycleBottomPane => {
596                self.bottom_pane = self.bottom_pane.next();
597            }
598            Action::EnterGraph => {
599                if self.editor != Editor::Graph || self.mode != Mode::Normal {
600                    return;
601                }
602                if self.graph_cursor >= self.graph_has_children.len() {
603                    return;
604                }
605                if !self.graph_has_children[self.graph_cursor] {
606                    return;
607                }
608                self.enter_nested_graph();
609            }
610            Action::ExitGraph => {
611                if self.editor != Editor::Graph {
612                    return;
613                }
614                self.exit_nested_graph();
615            }
616        }
617        if sync_scope {
618            self.sync_scope_focus();
619        }
620    }
621
622    fn current_node_param_count(&self) -> usize {
623        self.graph_params
624            .get(self.graph_cursor)
625            .map_or(0, |p| p.len())
626    }
627
628    fn current_node_description(&self) -> &str {
629        if self.editor != Editor::Graph {
630            return "";
631        }
632        self.graph_node_descriptions
633            .get(self.graph_cursor)
634            .map(|s| s.as_str())
635            .unwrap_or("")
636    }
637
638    fn current_param_help(&self) -> &str {
639        if self.editor != Editor::Graph || self.mode != crate::input::Mode::Edit {
640            return "";
641        }
642        self.graph_params
643            .get(self.graph_cursor)
644            .and_then(|params| params.get(self.param_cursor))
645            .map(|p| p.help)
646            .unwrap_or("")
647    }
648
649    fn adjust_param_coarse(&mut self, direction: i32) {
650        self.adjust_param_by(direction, false);
651    }
652
653    fn adjust_param_fine(&mut self, direction: i32) {
654        self.adjust_param_by(direction, true);
655    }
656
657    fn adjust_param_by(&mut self, direction: i32, fine: bool) {
658        let params = match self.graph_params.get(self.graph_cursor) {
659            Some(p) if !p.is_empty() => p,
660            _ => return,
661        };
662        let desc = match params.get(self.param_cursor) {
663            Some(d) => d,
664            None => return,
665        };
666        let values = match self.graph_param_values.get_mut(self.graph_cursor) {
667            Some(v) => v,
668            None => return,
669        };
670
671        let base_step = if desc.step > 0.0 {
672            desc.step
673        } else {
674            (desc.max - desc.min) * 0.01
675        };
676        let step = if fine { base_step * 0.1 } else { base_step };
677
678        let old = values[self.param_cursor];
679        let new_val = (old + step * direction as f64).clamp(desc.min, desc.max);
680        values[self.param_cursor] = new_val;
681
682        let node_id = self.graph_nodes[self.graph_cursor].0;
683        let mut path = self.graph_path.clone();
684        path.push(node_id);
685        self.bridge.send(Command::SetParam {
686            path,
687            param_id: desc.id,
688            value: new_val,
689        });
690
691        if !self.playing {
692            self.fire_param_preview();
693        }
694    }
695
696    /// Sends a short preview note so the user hears parameter changes even
697    /// when the transport is stopped. The note flows through the full graph
698    /// chain (synths → bus → FX → master), making all node tweaks audible.
699    fn fire_param_preview(&mut self) {
700        let voice = self.voice_ids.first().copied().unwrap_or(0);
701        if let Some((old, _)) = self.preview_note_off.take() {
702            self.bridge.send(Command::NoteOff { voice: old });
703        }
704        self.bridge.send(Command::NoteOn {
705            frequency: 440.0,
706            velocity: 0.6,
707            voice,
708        });
709        self.preview_note_off = Some((voice, Instant::now()));
710    }
711
712    fn next_rng(&mut self) -> u64 {
713        self.rng_state = self
714            .rng_state
715            .wrapping_mul(6364136223846793005)
716            .wrapping_add(1442695040888963407);
717        self.rng_state
718    }
719
720    fn randomize_current_voice(&mut self) {
721        let col = self.cursor_col;
722        let scale_len = self.scale.len() as i32;
723        for row in 0..self.grid.rows {
724            let r = self.next_rng();
725            if r % 100 < 40 {
726                let degree = (self.next_rng() % scale_len.max(1) as u64) as i32;
727                let vel_n = (self.next_rng() % 6 + 2) as i64; // 2..8
728                let event = NoteEvent::new(degree, self.octave, Rational::new(vel_n, 8));
729                self.grid.set(row, col, Some(event));
730            } else {
731                self.grid.set(row, col, None);
732            }
733        }
734    }
735
736    fn adjust_note_velocity(&mut self, delta: Rational) {
737        if let Some(note) = self.grid.get(self.cursor_row, self.cursor_col).cloned() {
738            let new_vel = note.velocity + delta;
739            let clamped = if new_vel.to_f64() < 0.0625 {
740                Rational::new(1, 16)
741            } else if new_vel.to_f64() > 1.0 {
742                Rational::new(1, 1)
743            } else {
744                new_vel
745            };
746            let mut updated = note;
747            updated.velocity = clamped;
748            self.grid
749                .set(self.cursor_row, self.cursor_col, Some(updated));
750        }
751    }
752
753    fn graph_move_up(&mut self) {
754        if self.graph_depths.is_empty() {
755            return;
756        }
757        let depth = self.graph_depths[self.graph_cursor];
758        let layer = &self.graph_layers[depth];
759        if let Some(pos) = layer.iter().position(|&i| i == self.graph_cursor) {
760            if pos > 0 {
761                self.graph_cursor = layer[pos - 1];
762            }
763        }
764    }
765
766    fn graph_move_down(&mut self) {
767        if self.graph_depths.is_empty() {
768            return;
769        }
770        let depth = self.graph_depths[self.graph_cursor];
771        let layer = &self.graph_layers[depth];
772        if let Some(pos) = layer.iter().position(|&i| i == self.graph_cursor) {
773            if pos + 1 < layer.len() {
774                self.graph_cursor = layer[pos + 1];
775            }
776        }
777    }
778
779    fn graph_move_right(&mut self) {
780        let current_id = self.graph_nodes[self.graph_cursor].0;
781        let mut seen = HashSet::new();
782        for e in &self.graph_edges {
783            if e.src_node == current_id && seen.insert(e.dst_node) {
784                if let Some(idx) = self
785                    .graph_nodes
786                    .iter()
787                    .position(|(id, _)| *id == e.dst_node)
788                {
789                    self.graph_cursor = idx;
790                    return;
791                }
792            }
793        }
794    }
795
796    fn graph_move_left(&mut self) {
797        let current_id = self.graph_nodes[self.graph_cursor].0;
798        let mut seen = HashSet::new();
799        for e in &self.graph_edges {
800            if e.dst_node == current_id && seen.insert(e.src_node) {
801                if let Some(idx) = self
802                    .graph_nodes
803                    .iter()
804                    .position(|(id, _)| *id == e.src_node)
805                {
806                    self.graph_cursor = idx;
807                    return;
808                }
809            }
810        }
811    }
812
813    fn enter_nested_graph(&mut self) {
814        let node_id = self.graph_nodes[self.graph_cursor].0;
815
816        self.graph_stack.push(GraphFrame {
817            nodes: self.graph_nodes.clone(),
818            edges: self.graph_edges.clone(),
819            cursor: self.graph_cursor,
820            params: self.graph_params.clone(),
821            param_values: self.graph_param_values.clone(),
822            param_groups: self.graph_param_groups.clone(),
823            depths: self.graph_depths.clone(),
824            layers: self.graph_layers.clone(),
825            has_children: self.graph_has_children.clone(),
826            node_descriptions: self.graph_node_descriptions.clone(),
827        });
828
829        let entered_name = self.graph_nodes[self.graph_cursor].1.clone();
830        self.graph_path.push(node_id);
831        self.graph_breadcrumb.push(entered_name);
832
833        if let Some(snap) = self.nested_graph_snapshots.get(&self.graph_path).cloned() {
834            self.load_graph_from_snapshot(&snap);
835        } else {
836            // No host snapshot for this path — keep empty placeholder until a bridge protocol exists.
837            self.graph_nodes = vec![];
838            self.graph_edges = vec![];
839            self.graph_depths = vec![];
840            self.graph_layers = vec![];
841            self.graph_params = vec![];
842            self.graph_param_values = vec![];
843            self.graph_param_groups = vec![];
844            self.graph_has_children = vec![];
845            self.graph_cursor = 0;
846        }
847    }
848
849    fn exit_nested_graph(&mut self) {
850        if let Some(frame) = self.graph_stack.pop() {
851            self.graph_nodes = frame.nodes;
852            self.graph_edges = frame.edges;
853            self.graph_cursor = frame.cursor;
854            self.graph_params = frame.params;
855            self.graph_param_values = frame.param_values;
856            self.graph_param_groups = frame.param_groups;
857            self.graph_depths = frame.depths;
858            self.graph_layers = frame.layers;
859            self.graph_has_children = frame.has_children;
860            self.graph_node_descriptions = frame.node_descriptions;
861            self.graph_path.pop();
862            self.graph_breadcrumb.pop();
863        }
864    }
865
866    /// Drains pending [`Notification`]s and timed preview note-off; call each frame from the UI loop.
867    pub fn poll_audio(&mut self) {
868        // Handle preview note release
869        if let Some((voice, time)) = self.preview_note_off {
870            if time.elapsed() > Duration::from_millis(120) {
871                self.bridge.send(Command::NoteOff { voice });
872                self.preview_note_off = None;
873            }
874        }
875
876        while let Some(notif) = self.bridge.try_recv() {
877            match notif {
878                Notification::Position { beat } => {
879                    self.beat_position = beat;
880                    let total_beats = self.grid.rows as f64;
881                    if total_beats > 0.0 {
882                        let row = (beat % total_beats) as u32;
883                        self.current_play_row = Some(row.min(self.grid.rows.saturating_sub(1)));
884                    }
885                }
886                Notification::ScopeData(snap) => {
887                    self.scope_master = snap.master;
888                    self.scope_graph_in = snap.graph_in;
889                }
890                Notification::Meter { peak_l, peak_r } => {
891                    self.peak_l = peak_l;
892                    self.peak_r = peak_r;
893                }
894                Notification::Stopped => {
895                    self.playing = false;
896                    self.engine_pattern_active = false;
897                    self.current_play_row = None;
898                }
899            }
900        }
901    }
902
903    #[inline]
904    fn should_sync_pattern(&self) -> bool {
905        self.playing || self.engine_pattern_active
906    }
907
908    fn send_pattern(&mut self) {
909        let beats = Rational::integer(self.grid.rows as i64);
910        let events = trem::render::grid_to_timed_events(
911            &self.grid,
912            beats,
913            self.bpm,
914            44100.0,
915            &self.scale,
916            440.0,
917            &self.voice_ids,
918            self.swing,
919        );
920        self.bridge.send(Command::LoadEvents(events));
921    }
922
923    fn push_undo(&mut self) {
924        self.undo_stack.push(self.grid.cells.clone());
925        if self.undo_stack.len() > 100 {
926            self.undo_stack.remove(0);
927        }
928        self.redo_stack.clear();
929    }
930
931    fn undo(&mut self) {
932        if let Some(snapshot) = self.undo_stack.pop() {
933            self.redo_stack.push(self.grid.cells.clone());
934            self.grid.cells = snapshot;
935            if self.should_sync_pattern() {
936                self.send_pattern();
937            }
938        }
939    }
940
941    fn redo(&mut self) {
942        if let Some(snapshot) = self.redo_stack.pop() {
943            self.undo_stack.push(self.grid.cells.clone());
944            self.grid.cells = snapshot;
945            if self.should_sync_pattern() {
946                self.send_pattern();
947            }
948        }
949    }
950
951    /// Lays out transport, sidebar, main view (pattern or graph), and scope into `frame`.
952    pub fn draw(&mut self, frame: &mut ratatui::Frame) {
953        self.refresh_host_stats();
954
955        let bottom_h = match self.editor {
956            Editor::Graph => 6u16,
957            Editor::Pattern => 5u16,
958        };
959        let outer = Layout::default()
960            .direction(Direction::Vertical)
961            .constraints([
962                Constraint::Length(1),
963                Constraint::Min(4),
964                Constraint::Length(bottom_h),
965            ])
966            .split(frame.area());
967
968        frame.render_widget(
969            TransportView {
970                bpm: self.bpm,
971                beat_position: self.beat_position,
972                playing: self.playing,
973                mode: &self.mode,
974                editor: &self.editor,
975                scale_name: &self.scale_name,
976                octave: self.octave,
977                swing: self.swing,
978                bottom_pane: self.bottom_pane,
979            },
980            outer[0],
981        );
982
983        let sidebar_w = info_sidebar_width(outer[1].width);
984        let middle = Layout::default()
985            .direction(Direction::Horizontal)
986            .constraints([
987                Constraint::Length(sidebar_w),
988                Constraint::Min(MAIN_EDITOR_MIN_WIDTH),
989            ])
990            .split(outer[1]);
991
992        let note_at_cursor = self.grid.get(self.cursor_row, self.cursor_col);
993        let graph_node_name = match self.editor {
994            Editor::Graph => self
995                .graph_nodes
996                .get(self.graph_cursor)
997                .map(|(_, n)| n.as_str()),
998            Editor::Pattern => None,
999        };
1000        let graph_can_enter_nested = matches!(self.editor, Editor::Graph)
1001            && self
1002                .graph_has_children
1003                .get(self.graph_cursor)
1004                .copied()
1005                .unwrap_or(false);
1006        let graph_is_nested = !self.graph_path.is_empty();
1007
1008        frame.render_widget(
1009            InfoView {
1010                mode: &self.mode,
1011                editor: &self.editor,
1012                octave: self.octave,
1013                cursor_step: self.cursor_row,
1014                cursor_voice: self.cursor_col,
1015                grid_steps: self.grid.rows,
1016                grid_voices: self.grid.columns,
1017                note_at_cursor,
1018                scale: &self.scale,
1019                scale_name: &self.scale_name,
1020                instrument_names: &self.instrument_names,
1021                swing: self.swing,
1022                euclidean_k: self.euclidean_k,
1023                undo_depth: self.undo_stack.len(),
1024                node_description: self.current_node_description(),
1025                param_help: self.current_param_help(),
1026                graph_node_name,
1027                graph_can_enter_nested,
1028                graph_is_nested,
1029                host_stats: &self.host_stats,
1030                peak_l: self.peak_l,
1031                peak_r: self.peak_r,
1032                playing: self.playing,
1033                bpm: self.bpm,
1034            },
1035            middle[0],
1036        );
1037
1038        match self.editor {
1039            Editor::Pattern => {
1040                frame.render_widget(
1041                    PatternView {
1042                        grid: &self.grid,
1043                        cursor_row: self.cursor_row,
1044                        cursor_col: self.cursor_col,
1045                        current_play_row: self.current_play_row,
1046                        mode: &self.mode,
1047                        scale: &self.scale,
1048                        instrument_names: &self.instrument_names,
1049                    },
1050                    middle[1],
1051                );
1052            }
1053            Editor::Graph => {
1054                let params = self.graph_params.get(self.graph_cursor);
1055                let values = self.graph_param_values.get(self.graph_cursor);
1056                let groups = self.graph_param_groups.get(self.graph_cursor);
1057                frame.render_widget(
1058                    GraphViewWidget {
1059                        nodes: &self.graph_nodes,
1060                        edges: &self.graph_edges,
1061                        selected: self.graph_cursor,
1062                        params: params.map(|p| p.as_slice()),
1063                        param_values: values.map(|v| v.as_slice()),
1064                        param_groups: groups.map(|g| g.as_slice()),
1065                        param_cursor: if self.mode == Mode::Edit {
1066                            Some(self.param_cursor)
1067                        } else {
1068                            None
1069                        },
1070                        breadcrumb: &self.graph_breadcrumb,
1071                        has_children: &self.graph_has_children,
1072                    },
1073                    middle[1],
1074                );
1075            }
1076        }
1077
1078        let now = Instant::now();
1079        self.spectrum_analyzer_in.fall_ms = self.spectrum_fall_ms;
1080        self.spectrum_analyzer_out.fall_ms = self.spectrum_fall_ms;
1081        let (spec_in, nr_in) = self.spectrum_analyzer_in.analyze(&self.scope_graph_in, now);
1082        let (spec_out, nr_out) = self.spectrum_analyzer_out.analyze(&self.scope_master, now);
1083
1084        match (self.editor, self.bottom_pane) {
1085            (Editor::Graph, BottomPane::Waveform) => {
1086                let chunks = Layout::default()
1087                    .direction(Direction::Horizontal)
1088                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1089                    .split(outer[2]);
1090                frame.render_widget(
1091                    ScopeView {
1092                        samples: &self.scope_graph_in,
1093                    },
1094                    chunks[0],
1095                );
1096                frame.render_widget(
1097                    ScopeView {
1098                        samples: &self.scope_master,
1099                    },
1100                    chunks[1],
1101                );
1102            }
1103            (Editor::Graph, BottomPane::Spectrum) => {
1104                let chunks = Layout::default()
1105                    .direction(Direction::Horizontal)
1106                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
1107                    .split(outer[2]);
1108                let fall = self.spectrum_fall_ms;
1109                frame.render_widget(
1110                    SpectrumView {
1111                        magnitudes: spec_in,
1112                        norm_ref: nr_in,
1113                        title: "IN",
1114                        decay_ms_label: fall,
1115                    },
1116                    chunks[0],
1117                );
1118                frame.render_widget(
1119                    SpectrumView {
1120                        magnitudes: spec_out,
1121                        norm_ref: nr_out,
1122                        title: "OUT",
1123                        decay_ms_label: fall,
1124                    },
1125                    chunks[1],
1126                );
1127            }
1128            (Editor::Pattern, BottomPane::Waveform) => {
1129                frame.render_widget(
1130                    ScopeView {
1131                        samples: &self.scope_master,
1132                    },
1133                    outer[2],
1134                );
1135            }
1136            (Editor::Pattern, BottomPane::Spectrum) => {
1137                frame.render_widget(
1138                    SpectrumView {
1139                        magnitudes: spec_out,
1140                        norm_ref: nr_out,
1141                        title: "OUT",
1142                        decay_ms_label: self.spectrum_fall_ms,
1143                    },
1144                    outer[2],
1145                );
1146            }
1147        }
1148
1149        if self.help_open {
1150            frame.render_widget(HelpOverlay, frame.area());
1151        }
1152    }
1153
1154    /// Terminal main loop until quit: render, handle keys, poll notifications.
1155    pub fn run<B>(mut self, terminal: &mut ratatui::Terminal<B>) -> anyhow::Result<()>
1156    where
1157        B: ratatui::backend::Backend,
1158        B::Error: std::error::Error + Send + Sync + 'static,
1159    {
1160        self.sync_scope_focus();
1161        loop {
1162            terminal.draw(|frame| self.draw(frame))?;
1163
1164            if event::poll(Duration::from_millis(16))? {
1165                if let Event::Key(key) = event::read()? {
1166                    if key.kind != KeyEventKind::Release {
1167                        let ctx = InputContext {
1168                            editor: self.editor,
1169                            mode: &self.mode,
1170                            graph_is_nested: !self.graph_path.is_empty(),
1171                            help_open: self.help_open,
1172                        };
1173                        if let Some(action) = input::handle_key(key, &ctx) {
1174                            self.handle_action(action);
1175                        }
1176                    }
1177                }
1178            }
1179
1180            self.poll_audio();
1181
1182            if self.should_quit {
1183                break;
1184            }
1185        }
1186        Ok(())
1187    }
1188}
1189
1190#[cfg(test)]
1191mod sidebar_width_tests {
1192    use super::{info_sidebar_width, MAIN_EDITOR_MIN_WIDTH};
1193
1194    #[test]
1195    fn narrow_middle_reserves_main_column() {
1196        let sw = info_sidebar_width(52);
1197        assert!(sw + MAIN_EDITOR_MIN_WIDTH <= 52, "sidebar={sw}");
1198    }
1199
1200    #[test]
1201    fn sidebar_caps_on_wide_terminals() {
1202        let sw = info_sidebar_width(200);
1203        assert!(sw <= 30, "sidebar={sw}");
1204    }
1205}