2026-09-13 14:06:12 +02:00
|
|
|
//! 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.
|
2026-09-13 13:24:18 +02:00
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
use std::io::{self, Write};
|
2026-09-13 13:24:18 +02:00
|
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
|
|
use std::sync::Arc;
|
2026-09-13 14:06:12 +02:00
|
|
|
use std::time::Duration;
|
2026-09-13 13:24:18 +02:00
|
|
|
|
|
|
|
|
use arc_swap::ArcSwap;
|
2026-09-13 14:06:12 +02:00
|
|
|
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};
|
2026-09-13 13:24:18 +02:00
|
|
|
|
|
|
|
|
use crate::dsp::{compile, Program};
|
|
|
|
|
use crate::grid::{Grid, H, W};
|
|
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
// 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,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-13 13:24:18 +02:00
|
|
|
|
|
|
|
|
pub fn run(
|
|
|
|
|
prog: Arc<ArcSwap<Program>>,
|
|
|
|
|
voices: Arc<AtomicU32>,
|
|
|
|
|
mut grid: Grid,
|
|
|
|
|
latency: u32,
|
2026-09-13 14:06:12 +02:00
|
|
|
) -> io::Result<()> {
|
|
|
|
|
enable_raw_mode()?;
|
|
|
|
|
let mut out = io::stdout();
|
|
|
|
|
execute!(out, EnterAlternateScreen, Hide)?;
|
2026-09-13 13:24:18 +02:00
|
|
|
let (mut cx, mut cy) = (0usize, 0usize);
|
|
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
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<ArcSwap<Program>>,
|
|
|
|
|
voices: &Arc<AtomicU32>,
|
|
|
|
|
grid: &mut Grid,
|
|
|
|
|
latency: u32,
|
|
|
|
|
cx: &mut usize,
|
|
|
|
|
cy: &mut usize,
|
|
|
|
|
) -> io::Result<()> {
|
2026-09-13 13:24:18 +02:00
|
|
|
loop {
|
2026-09-13 14:06:12 +02:00
|
|
|
draw(out, grid, *cx, *cy, voices.load(Ordering::Relaxed), latency)?;
|
2026-09-13 13:24:18 +02:00
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
if event::poll(Duration::from_millis(33))? {
|
|
|
|
|
if let Event::Key(k) = event::read()? {
|
2026-09-13 13:24:18 +02:00
|
|
|
if k.kind != KeyEventKind::Press {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if k.code == KeyCode::Esc
|
|
|
|
|
|| (k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL))
|
|
|
|
|
{
|
2026-09-13 14:06:12 +02:00
|
|
|
return Ok(());
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
let mut changed = false;
|
|
|
|
|
match k.code {
|
2026-09-13 14:06:12 +02:00
|
|
|
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),
|
2026-09-13 13:24:18 +02:00
|
|
|
KeyCode::Backspace | KeyCode::Delete => {
|
2026-09-13 14:06:12 +02:00
|
|
|
grid.set(*cx, *cy, '.');
|
2026-09-13 13:24:18 +02:00
|
|
|
changed = true;
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char(c) => {
|
2026-09-13 14:06:12 +02:00
|
|
|
grid.set(*cx, *cy, if c == ' ' { '.' } else { c });
|
2026-09-13 13:24:18 +02:00
|
|
|
changed = true;
|
2026-09-13 14:06:12 +02:00
|
|
|
*cx = (*cx + 1).min(W - 1);
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
if changed {
|
2026-09-13 14:06:12 +02:00
|
|
|
prog.store(Arc::new(compile(grid)));
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
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;
|
2026-09-13 13:24:18 +02:00
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
queue!(out, BeginSynchronizedUpdate, Clear(ClearType::All))?;
|
2026-09-13 13:24:18 +02:00
|
|
|
for row in 0..H {
|
2026-09-13 14:06:12 +02:00
|
|
|
queue!(out, MoveTo(off_x as u16, (off_y + row) as u16))?;
|
2026-09-13 13:24:18 +02:00
|
|
|
for col in 0..W {
|
2026-09-13 14:06:12 +02:00
|
|
|
let c = grid.get(col, row);
|
|
|
|
|
queue!(out, SetAttribute(Attribute::Reset))?;
|
2026-09-13 13:24:18 +02:00
|
|
|
if col == cx && row == cy {
|
2026-09-13 14:06:12 +02:00
|
|
|
queue!(out, SetAttribute(Attribute::Reverse))?;
|
|
|
|
|
} else {
|
|
|
|
|
queue!(out, SetForegroundColor(glyph_color(c)))?;
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
2026-09-13 14:06:12 +02:00
|
|
|
queue!(out, Print(c))?;
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 14:06:12 +02:00
|
|
|
// Status line beneath the grid.
|
2026-09-13 13:24:18 +02:00
|
|
|
let status = format!(
|
2026-09-13 14:06:12 +02:00
|
|
|
"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"
|
2026-09-13 13:24:18 +02:00
|
|
|
);
|
2026-09-13 14:06:12 +02:00
|
|
|
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()
|
2026-09-13 13:24:18 +02:00
|
|
|
}
|