//! Grid editor. crossterm renderer adapted from orca-rs's grid view (same //! centered-canvas + per-glyph colour approach); no embedded scope - run a //! spectrum analyzer (e.g. spectrust) on the side if you want one. use std::io::{self, Write}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; use std::time::Duration; use arc_swap::ArcSwap; use crossterm::cursor::{Hide, MoveTo, Show}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::style::{Attribute, Color, Print, SetAttribute, SetForegroundColor}; use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, }; use crossterm::{execute, queue, terminal}; use crate::dsp::{compile, Program}; use crate::grid::{Grid, H, W}; // Foreground colour by glyph category. (orca-rs uses one Normal colour; galdr // tints by role so a patch reads at a glance.) fn glyph_color(c: char) -> Color { match c { 'O' | 'A' | 'U' | 'V' | 'N' => Color::Cyan, // sources 'L' | 'H' | 'B' => Color::Blue, // filters 'S' | 'C' | 'F' | 'D' => Color::Yellow, // shapers 'E' | 'G' | 'R' | 'M' => Color::Green, // amp / mod '!' => Color::Magenta, // output '0'..='9' => Color::Grey, // constants 'a'..='z' => Color::DarkYellow, // CC registers '.' => Color::DarkGrey, // empty _ => Color::White, } } pub fn run( prog: Arc>, voices: Arc, mut grid: Grid, latency: u32, ) -> io::Result<()> { enable_raw_mode()?; let mut out = io::stdout(); execute!(out, EnterAlternateScreen, Hide)?; let (mut cx, mut cy) = (0usize, 0usize); let res = main_loop(&mut out, &prog, &voices, &mut grid, latency, &mut cx, &mut cy); execute!(out, Show, LeaveAlternateScreen)?; disable_raw_mode()?; res } fn main_loop( out: &mut io::Stdout, prog: &Arc>, voices: &Arc, grid: &mut Grid, latency: u32, cx: &mut usize, cy: &mut usize, ) -> io::Result<()> { loop { draw(out, grid, *cx, *cy, voices.load(Ordering::Relaxed), latency)?; if event::poll(Duration::from_millis(33))? { if let Event::Key(k) = event::read()? { if k.kind != KeyEventKind::Press { continue; } if k.code == KeyCode::Esc || (k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL)) { return Ok(()); } let mut changed = false; match k.code { KeyCode::Left => *cx = cx.saturating_sub(1), KeyCode::Right => *cx = (*cx + 1).min(W - 1), KeyCode::Up => *cy = cy.saturating_sub(1), KeyCode::Down => *cy = (*cy + 1).min(H - 1), KeyCode::Backspace | KeyCode::Delete => { grid.set(*cx, *cy, '.'); changed = true; } KeyCode::Char(c) => { grid.set(*cx, *cy, if c == ' ' { '.' } else { c }); changed = true; *cx = (*cx + 1).min(W - 1); } _ => {} } if changed { prog.store(Arc::new(compile(grid))); } } } } } fn draw( out: &mut io::Stdout, grid: &Grid, cx: usize, cy: usize, voices: u32, latency: u32, ) -> io::Result<()> { let (tw, th) = terminal::size()?; let off_x = (tw as usize).saturating_sub(W) / 2; let off_y = (th as usize).saturating_sub(H + 1) / 2; queue!(out, BeginSynchronizedUpdate, Clear(ClearType::All))?; for row in 0..H { queue!(out, MoveTo(off_x as u16, (off_y + row) as u16))?; for col in 0..W { let c = grid.get(col, row); queue!(out, SetAttribute(Attribute::Reset))?; if col == cx && row == cy { queue!(out, SetAttribute(Attribute::Reverse))?; } else { queue!(out, SetForegroundColor(glyph_color(c)))?; } queue!(out, Print(c))?; } } // Status line beneath the grid. let status = format!( "voices {voices} latency {latency}f O~ A^ U pulse V tri N noise | L H B filt | S C F D shape | E env G gain R ring M mix | ! out Esc quit" ); let s: String = status.chars().take(tw as usize).collect(); queue!( out, MoveTo(0, th.saturating_sub(1)), SetAttribute(Attribute::Reset), SetForegroundColor(Color::DarkGrey), Print(s) )?; queue!(out, EndSynchronizedUpdate)?; out.flush() }