Fixes
This commit is contained in:
@@ -190,9 +190,7 @@ impl Forth {
|
||||
outputs: &mut Vec<String>,
|
||||
cmd: &mut CmdRegister|
|
||||
-> Result<(), String> {
|
||||
if stack.len() < count {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
let selected = stack[start + idx].clone();
|
||||
stack.truncate(start);
|
||||
@@ -280,106 +278,81 @@ impl Forth {
|
||||
Op::PushStr(s, span) => stack.push(Value::Str(s.clone(), *span)),
|
||||
|
||||
Op::Dup => {
|
||||
let v = stack.last().ok_or("stack underflow")?.clone();
|
||||
ensure(stack, 1)?;
|
||||
let v = stack.last().unwrap().clone();
|
||||
stack.push(v);
|
||||
}
|
||||
Op::Dupn => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let n = pop_int(stack)?;
|
||||
let v = pop(stack)?;
|
||||
for _ in 0..n {
|
||||
stack.push(v.clone());
|
||||
}
|
||||
}
|
||||
Op::Drop => {
|
||||
stack.pop().ok_or("stack underflow")?;
|
||||
pop(stack)?;
|
||||
}
|
||||
Op::Swap => {
|
||||
ensure(stack, 2)?;
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
stack.swap(len - 1, len - 2);
|
||||
}
|
||||
Op::Over => {
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let v = stack[len - 2].clone();
|
||||
ensure(stack, 2)?;
|
||||
let v = stack[stack.len() - 2].clone();
|
||||
stack.push(v);
|
||||
}
|
||||
Op::Rot => {
|
||||
let len = stack.len();
|
||||
if len < 3 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let v = stack.remove(len - 3);
|
||||
ensure(stack, 3)?;
|
||||
let v = stack.remove(stack.len() - 3);
|
||||
stack.push(v);
|
||||
}
|
||||
Op::Nip => {
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
stack.remove(len - 2);
|
||||
ensure(stack, 2)?;
|
||||
stack.remove(stack.len() - 2);
|
||||
}
|
||||
Op::Tuck => {
|
||||
ensure(stack, 2)?;
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let v = stack[len - 1].clone();
|
||||
stack.insert(len - 2, v);
|
||||
}
|
||||
Op::Dup2 => {
|
||||
ensure(stack, 2)?;
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let a = stack[len - 2].clone();
|
||||
let b = stack[len - 1].clone();
|
||||
stack.push(a);
|
||||
stack.push(b);
|
||||
}
|
||||
Op::Drop2 => {
|
||||
let len = stack.len();
|
||||
if len < 2 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 2)?;
|
||||
stack.pop();
|
||||
stack.pop();
|
||||
}
|
||||
Op::Swap2 => {
|
||||
ensure(stack, 4)?;
|
||||
let len = stack.len();
|
||||
if len < 4 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
stack.swap(len - 4, len - 2);
|
||||
stack.swap(len - 3, len - 1);
|
||||
}
|
||||
Op::Over2 => {
|
||||
ensure(stack, 4)?;
|
||||
let len = stack.len();
|
||||
if len < 4 {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let a = stack[len - 4].clone();
|
||||
let b = stack[len - 3].clone();
|
||||
stack.push(a);
|
||||
stack.push(b);
|
||||
}
|
||||
Op::Rev => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
stack[start..].reverse();
|
||||
}
|
||||
Op::Shuffle => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
let slice = &mut stack[start..];
|
||||
let mut rng = self.rng.lock();
|
||||
@@ -389,10 +362,8 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
Op::Sort => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
stack[start..].sort_by(|a, b| {
|
||||
a.as_float()
|
||||
@@ -402,10 +373,8 @@ impl Forth {
|
||||
});
|
||||
}
|
||||
Op::RSort => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
stack[start..].sort_by(|a, b| {
|
||||
b.as_float()
|
||||
@@ -415,10 +384,8 @@ impl Forth {
|
||||
});
|
||||
}
|
||||
Op::Sum => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
let total: f64 = stack
|
||||
.drain(start..)
|
||||
@@ -427,10 +394,8 @@ impl Forth {
|
||||
stack.push(float_to_value(total));
|
||||
}
|
||||
Op::Prod => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let count = pop_int(stack)? as usize;
|
||||
ensure(stack, count)?;
|
||||
let start = stack.len() - count;
|
||||
let product: f64 = stack
|
||||
.drain(start..)
|
||||
@@ -443,16 +408,16 @@ impl Forth {
|
||||
Op::Sub => binary_op(stack, |a, b| a - b)?,
|
||||
Op::Mul => binary_op(stack, |a, b| a * b)?,
|
||||
Op::Div => {
|
||||
let b = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let b = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
if b.as_float().map_or(true, |v| v == 0.0) {
|
||||
return Err("division by zero".into());
|
||||
}
|
||||
stack.push(lift_binary(a, b, |x, y| x / y)?);
|
||||
}
|
||||
Op::Mod => {
|
||||
let b = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let b = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
if b.as_float().map_or(true, |v| v == 0.0) {
|
||||
return Err("modulo by zero".into());
|
||||
}
|
||||
@@ -460,42 +425,42 @@ impl Forth {
|
||||
stack.push(result);
|
||||
}
|
||||
Op::Neg => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| -x)?);
|
||||
}
|
||||
Op::Abs => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.abs())?);
|
||||
}
|
||||
Op::Floor => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.floor())?);
|
||||
}
|
||||
Op::Ceil => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.ceil())?);
|
||||
}
|
||||
Op::Round => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.round())?);
|
||||
}
|
||||
Op::Min => binary_op(stack, |a, b| a.min(b))?,
|
||||
Op::Max => binary_op(stack, |a, b| a.max(b))?,
|
||||
Op::Pow => binary_op(stack, |a, b| a.powf(b))?,
|
||||
Op::Sqrt => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.sqrt())?);
|
||||
}
|
||||
Op::Sin => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.sin())?);
|
||||
}
|
||||
Op::Cos => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.cos())?);
|
||||
}
|
||||
Op::Log => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
stack.push(lift_unary(v, |x| x.ln())?);
|
||||
}
|
||||
|
||||
@@ -507,37 +472,37 @@ impl Forth {
|
||||
Op::Ge => cmp_op(stack, |a, b| a >= b)?,
|
||||
|
||||
Op::And => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let b = pop_bool(stack)?;
|
||||
let a = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if a && b { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Or => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let b = pop_bool(stack)?;
|
||||
let a = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if a || b { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Not => {
|
||||
let v = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let v = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if v { 0 } else { 1 }, None));
|
||||
}
|
||||
Op::Xor => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let b = pop_bool(stack)?;
|
||||
let a = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if a ^ b { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Nand => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let b = pop_bool(stack)?;
|
||||
let a = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if !(a && b) { 1 } else { 0 }, None));
|
||||
}
|
||||
Op::Nor => {
|
||||
let b = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let a = stack.pop().ok_or("stack underflow")?.is_truthy();
|
||||
let b = pop_bool(stack)?;
|
||||
let a = pop_bool(stack)?;
|
||||
stack.push(Value::Int(if !(a || b) { 1 } else { 0 }, None));
|
||||
}
|
||||
|
||||
Op::BranchIfZero(offset, then_span, else_span) => {
|
||||
let v = stack.pop().ok_or("stack underflow")?;
|
||||
let v = pop(stack)?;
|
||||
if !v.is_truthy() {
|
||||
if let Some(span) = else_span {
|
||||
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
|
||||
@@ -556,9 +521,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::NewCmd => {
|
||||
if stack.is_empty() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
let val = if values.len() == 1 {
|
||||
values.into_iter().next().unwrap()
|
||||
@@ -568,9 +531,7 @@ impl Forth {
|
||||
cmd.set_sound(val);
|
||||
}
|
||||
Op::SetParam(param) => {
|
||||
if stack.is_empty() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
let val = if values.len() == 1 {
|
||||
values.into_iter().next().unwrap()
|
||||
@@ -654,7 +615,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Get => {
|
||||
let name = stack.pop().ok_or("stack underflow")?;
|
||||
let name = pop(stack)?;
|
||||
let name = name.as_str()?;
|
||||
let vw = var_writes_cell.borrow();
|
||||
let vw_ref = vw.as_ref().expect("var_writes taken");
|
||||
@@ -667,9 +628,9 @@ impl Forth {
|
||||
stack.push(val);
|
||||
}
|
||||
Op::Set => {
|
||||
let name = stack.pop().ok_or("stack underflow")?;
|
||||
let name = pop(stack)?;
|
||||
let name = name.as_str()?.to_string();
|
||||
let val = stack.pop().ok_or("stack underflow")?;
|
||||
let val = pop(stack)?;
|
||||
var_writes_cell
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
@@ -703,8 +664,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Rand(word_span) => {
|
||||
let b = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let b = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
match (&a, &b) {
|
||||
(Value::Int(a_i, _), Value::Int(b_i, _)) => {
|
||||
let (lo, hi) = if a_i <= b_i {
|
||||
@@ -731,8 +692,8 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
Op::ExpRand(word_span) => {
|
||||
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let hi = pop_float(stack)?;
|
||||
let lo = pop_float(stack)?;
|
||||
if lo <= 0.0 || hi <= 0.0 {
|
||||
return Err("exprand requires positive values".into());
|
||||
}
|
||||
@@ -743,8 +704,8 @@ impl Forth {
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::LogRand(word_span) => {
|
||||
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let lo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let hi = pop_float(stack)?;
|
||||
let lo = pop_float(stack)?;
|
||||
if lo <= 0.0 || hi <= 0.0 {
|
||||
return Err("logrand requires positive values".into());
|
||||
}
|
||||
@@ -755,12 +716,12 @@ impl Forth {
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::Seed => {
|
||||
let s = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let s = pop_int(stack)?;
|
||||
*self.rng.lock() = StdRng::seed_from_u64(s as u64);
|
||||
}
|
||||
|
||||
Op::Cycle(word_span) | Op::PCycle(word_span) => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("cycle count must be > 0".into());
|
||||
}
|
||||
@@ -779,7 +740,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Choose(word_span) => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("choose count must be > 0".into());
|
||||
}
|
||||
@@ -795,7 +756,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Bounce(word_span) => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("bounce count must be > 0".into());
|
||||
}
|
||||
@@ -817,14 +778,12 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::WChoose(word_span) => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
let count = pop_int(stack)? as usize;
|
||||
if count == 0 {
|
||||
return Err("wchoose count must be > 0".into());
|
||||
}
|
||||
let pairs_needed = count * 2;
|
||||
if stack.len() < pairs_needed {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, pairs_needed)?;
|
||||
let start = stack.len() - pairs_needed;
|
||||
let mut values = Vec::with_capacity(count);
|
||||
let mut weights = Vec::with_capacity(count);
|
||||
@@ -858,8 +817,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::ChanceExec(word_span) | Op::ProbExec(word_span) => {
|
||||
let threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let threshold = pop_float(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
let val: f64 = self.rng.lock().gen();
|
||||
let limit = match &ops[pc] {
|
||||
Op::ChanceExec(_) => threshold,
|
||||
@@ -880,7 +839,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Every => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let n = pop_int(stack)?;
|
||||
if n <= 0 {
|
||||
return Err("every count must be > 0".into());
|
||||
}
|
||||
@@ -893,8 +852,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::When | Op::Unless => {
|
||||
let cond = stack.pop().ok_or("stack underflow")?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let cond = pop(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
let should_run = match &ops[pc] {
|
||||
Op::When => cond.is_truthy(),
|
||||
_ => !cond.is_truthy(),
|
||||
@@ -905,9 +864,9 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::IfElse => {
|
||||
let cond = stack.pop().ok_or("stack underflow")?;
|
||||
let false_quot = stack.pop().ok_or("stack underflow")?;
|
||||
let true_quot = stack.pop().ok_or("stack underflow")?;
|
||||
let cond = pop(stack)?;
|
||||
let false_quot = pop(stack)?;
|
||||
let true_quot = pop(stack)?;
|
||||
let quot = if cond.is_truthy() {
|
||||
true_quot
|
||||
} else {
|
||||
@@ -917,7 +876,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Pick => {
|
||||
let idx_i = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let idx_i = pop_int(stack)?;
|
||||
if idx_i < 0 {
|
||||
return Err(format!("pick index must be >= 0, got {idx_i}"));
|
||||
}
|
||||
@@ -944,13 +903,13 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Mtof => {
|
||||
let note = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let note = pop_float(stack)?;
|
||||
let freq = 440.0 * 2.0_f64.powf((note - 69.0) / 12.0);
|
||||
stack.push(Value::Float(freq, None));
|
||||
}
|
||||
|
||||
Op::Ftom => {
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let freq = pop_float(stack)?;
|
||||
let note = 69.0 + 12.0 * (freq / 440.0).log2();
|
||||
stack.push(Value::Float(note, None));
|
||||
}
|
||||
@@ -959,7 +918,7 @@ impl Forth {
|
||||
if pattern.is_empty() {
|
||||
return Err("empty scale pattern".into());
|
||||
}
|
||||
let val = stack.pop().ok_or("stack underflow")?;
|
||||
let val = pop(stack)?;
|
||||
let len = pattern.len() as i64;
|
||||
let result = lift_unary_int(val, |degree| {
|
||||
let octave_offset = degree.div_euclid(len);
|
||||
@@ -970,21 +929,21 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Chord(intervals) => {
|
||||
let root = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let root = pop_int(stack)?;
|
||||
for &interval in *intervals {
|
||||
stack.push(Value::Int(root + interval, None));
|
||||
}
|
||||
}
|
||||
|
||||
Op::Oct => {
|
||||
let shift = stack.pop().ok_or("stack underflow")?;
|
||||
let note = stack.pop().ok_or("stack underflow")?;
|
||||
let shift = pop(stack)?;
|
||||
let note = pop(stack)?;
|
||||
let result = lift_binary(note, shift, |n, s| n + s * 12.0)?;
|
||||
stack.push(result);
|
||||
}
|
||||
|
||||
Op::SetTempo => {
|
||||
let tempo = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let tempo = pop_float(stack)?;
|
||||
let clamped = tempo.clamp(20.0, 300.0);
|
||||
var_writes_cell
|
||||
.borrow_mut()
|
||||
@@ -994,7 +953,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::SetSpeed => {
|
||||
let speed = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let speed = pop_float(stack)?;
|
||||
let clamped = speed.clamp(0.125, 8.0);
|
||||
var_writes_cell
|
||||
.borrow_mut()
|
||||
@@ -1004,8 +963,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Chain => {
|
||||
let pattern = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
||||
let bank = stack.pop().ok_or("stack underflow")?.as_int()? - 1;
|
||||
let pattern = pop_int(stack)? - 1;
|
||||
let bank = pop_int(stack)? - 1;
|
||||
if bank < 0 || pattern < 0 {
|
||||
return Err("chain: bank and pattern must be >= 1".into());
|
||||
}
|
||||
@@ -1024,7 +983,7 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Loop => {
|
||||
let beats = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let beats = pop_float(stack)?;
|
||||
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
|
||||
return Err("tempo and speed must be non-zero".into());
|
||||
}
|
||||
@@ -1034,26 +993,22 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::At => {
|
||||
if stack.is_empty() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 1)?;
|
||||
let deltas = std::mem::take(stack);
|
||||
cmd.set_deltas(deltas);
|
||||
}
|
||||
|
||||
Op::Arp => {
|
||||
if stack.is_empty() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
stack.push(Value::ArpList(Arc::from(values)));
|
||||
}
|
||||
|
||||
Op::Adsr => {
|
||||
let r = stack.pop().ok_or("stack underflow")?;
|
||||
let s = stack.pop().ok_or("stack underflow")?;
|
||||
let d = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let r = pop(stack)?;
|
||||
let s = pop(stack)?;
|
||||
let d = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
cmd.set_param("attack", a);
|
||||
cmd.set_param("decay", d);
|
||||
cmd.set_param("sustain", s);
|
||||
@@ -1061,41 +1016,41 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Ad => {
|
||||
let d = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let d = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
cmd.set_param("attack", a);
|
||||
cmd.set_param("decay", d);
|
||||
cmd.set_param("sustain", Value::Int(0, None));
|
||||
}
|
||||
|
||||
Op::Apply => {
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let quot = pop(stack)?;
|
||||
run_quotation(quot, stack, outputs, cmd)?;
|
||||
}
|
||||
|
||||
Op::Ramp => {
|
||||
let curve = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let curve = pop_float(stack)?;
|
||||
let freq = pop_float(stack)?;
|
||||
let phase = (freq * ctx.beat).fract();
|
||||
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
|
||||
let val = phase.powf(curve);
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::Triangle => {
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let freq = pop_float(stack)?;
|
||||
let phase = (freq * ctx.beat).fract();
|
||||
let phase = if phase < 0.0 { phase + 1.0 } else { phase };
|
||||
let val = 1.0 - (2.0 * phase - 1.0).abs();
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
Op::Range => {
|
||||
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let val = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
let val = pop_float(stack)?;
|
||||
stack.push(Value::Float(min + val * (max - min), None));
|
||||
}
|
||||
Op::Perlin => {
|
||||
let freq = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let freq = pop_float(stack)?;
|
||||
let val = perlin_noise_1d(freq * ctx.beat);
|
||||
stack.push(Value::Float(val, None));
|
||||
}
|
||||
@@ -1105,8 +1060,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::IntRange => {
|
||||
let end = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let end = pop_int(stack)?;
|
||||
let start = pop_int(stack)?;
|
||||
if start <= end {
|
||||
for i in start..=end {
|
||||
stack.push(Value::Int(i, None));
|
||||
@@ -1119,9 +1074,9 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::StepRange => {
|
||||
let step = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let step = pop_float(stack)?;
|
||||
let end = pop_float(stack)?;
|
||||
let start = pop_float(stack)?;
|
||||
if step == 0.0 {
|
||||
return Err("step cannot be zero".into());
|
||||
}
|
||||
@@ -1138,8 +1093,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Generate => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let count = pop_int(stack)?;
|
||||
let quot = pop(stack)?;
|
||||
if count < 0 {
|
||||
return Err("gen count must be >= 0".into());
|
||||
}
|
||||
@@ -1154,8 +1109,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Times => {
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let quot = pop(stack)?;
|
||||
let count = pop_int(stack)?;
|
||||
if count < 0 {
|
||||
return Err("times count must be >= 0".into());
|
||||
}
|
||||
@@ -1170,9 +1125,9 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::GeomRange => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let ratio = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let count = pop_int(stack)?;
|
||||
let ratio = pop_float(stack)?;
|
||||
let start = pop_float(stack)?;
|
||||
if count < 0 {
|
||||
return Err("geom.. count must be >= 0".into());
|
||||
}
|
||||
@@ -1184,8 +1139,8 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::Euclid => {
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let k = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let n = pop_int(stack)?;
|
||||
let k = pop_int(stack)?;
|
||||
if k < 0 || n < 0 {
|
||||
return Err("euclid: k and n must be >= 0".into());
|
||||
}
|
||||
@@ -1195,9 +1150,9 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::EuclidRot => {
|
||||
let r = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let n = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let k = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let r = pop_int(stack)?;
|
||||
let n = pop_int(stack)?;
|
||||
let k = pop_int(stack)?;
|
||||
if k < 0 || n < 0 || r < 0 {
|
||||
return Err("euclidrot: k, n, and r must be >= 0".into());
|
||||
}
|
||||
@@ -1207,33 +1162,31 @@ impl Forth {
|
||||
}
|
||||
|
||||
Op::ModLfo(shape) => {
|
||||
let period = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
|
||||
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let period = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
let suffix = match shape { 1 => "t", 2 => "w", 3 => "q", _ => "" };
|
||||
let s = format!("{min}~{max}:{period}{suffix}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
Op::ModSlide(curve) => {
|
||||
let dur = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
|
||||
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let dur = pop_float(stack)? * ctx.step_duration();
|
||||
let end = pop_float(stack)?;
|
||||
let start = pop_float(stack)?;
|
||||
let suffix = match curve { 1 => "e", 2 => "s", _ => "" };
|
||||
let s = format!("{start}>{end}:{dur}{suffix}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
Op::ModRnd(dist) => {
|
||||
let period = stack.pop().ok_or("stack underflow")?.as_float()? * ctx.step_duration();
|
||||
let max = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let min = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let period = pop_float(stack)? * ctx.step_duration();
|
||||
let max = pop_float(stack)?;
|
||||
let min = pop_float(stack)?;
|
||||
let suffix = match dist { 1 => "s", 2 => "d", _ => "" };
|
||||
let s = format!("{min}?{max}:{period}{suffix}");
|
||||
stack.push(Value::Str(s.into(), None));
|
||||
}
|
||||
Op::ModEnv => {
|
||||
if stack.is_empty() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
ensure(stack, 1)?;
|
||||
let values = std::mem::take(stack);
|
||||
let mut floats = Vec::with_capacity(values.len());
|
||||
for v in &values {
|
||||
@@ -1319,8 +1272,8 @@ impl Forth {
|
||||
outputs.push(format!("/midi/continue/dev/{dev}"));
|
||||
}
|
||||
Op::GetMidiCC => {
|
||||
let chan = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let cc = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let chan = pop_int(stack)?;
|
||||
let cc = pop_int(stack)?;
|
||||
let cc_clamped = (cc.clamp(0, 127)) as usize;
|
||||
let chan_clamped = (chan.clamp(1, 16) - 1) as usize;
|
||||
let (_, params) = cmd.snapshot().unwrap_or((None, &[]));
|
||||
@@ -1333,8 +1286,8 @@ impl Forth {
|
||||
stack.push(Value::Int(val as i64, None));
|
||||
}
|
||||
Op::Forget => {
|
||||
let name = stack.pop().ok_or("stack underflow")?.as_str()?.to_string();
|
||||
self.dict.lock().remove(&name);
|
||||
let name = pop(stack)?;
|
||||
self.dict.lock().remove(name.as_str()?);
|
||||
}
|
||||
}
|
||||
pc += 1;
|
||||
@@ -1543,6 +1496,29 @@ fn perlin_noise_1d(x: f64) -> f64 {
|
||||
(d0 + s * (d1 - d0)) * 0.5 + 0.5
|
||||
}
|
||||
|
||||
fn pop(stack: &mut Vec<Value>) -> Result<Value, String> {
|
||||
stack.pop().ok_or_else(|| "stack underflow".to_string())
|
||||
}
|
||||
|
||||
fn pop_int(stack: &mut Vec<Value>) -> Result<i64, String> {
|
||||
pop(stack)?.as_int()
|
||||
}
|
||||
|
||||
fn pop_float(stack: &mut Vec<Value>) -> Result<f64, String> {
|
||||
pop(stack)?.as_float()
|
||||
}
|
||||
|
||||
fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
|
||||
Ok(pop(stack)?.is_truthy())
|
||||
}
|
||||
|
||||
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
|
||||
if stack.len() < n {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn float_to_value(result: f64) -> Value {
|
||||
if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
|
||||
Value::Int(result as i64, None)
|
||||
@@ -1576,8 +1552,8 @@ fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||
where
|
||||
F: Fn(f64, f64) -> f64 + Copy,
|
||||
{
|
||||
let b = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let b = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
stack.push(lift_binary(a, b, f)?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1586,8 +1562,8 @@ fn cmp_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>
|
||||
where
|
||||
F: Fn(f64, f64) -> bool,
|
||||
{
|
||||
let b = stack.pop().ok_or("stack underflow")?;
|
||||
let a = stack.pop().ok_or("stack underflow")?;
|
||||
let b = pop(stack)?;
|
||||
let a = pop(stack)?;
|
||||
let result = if f(a.as_float()?, b.as_float()?) {
|
||||
1
|
||||
} else {
|
||||
|
||||
@@ -24,19 +24,23 @@ impl Widget for Spectrum<'_> {
|
||||
|
||||
let colors = theme::get();
|
||||
let height = area.height as f32;
|
||||
let band_width = area.width as usize / 32;
|
||||
if band_width == 0 {
|
||||
let base = area.width as usize / 32;
|
||||
let remainder = area.width as usize % 32;
|
||||
if base == 0 && remainder == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut x_start = area.x;
|
||||
for (band, &mag) in self.data.iter().enumerate() {
|
||||
let w = base + if band < remainder { 1 } else { 0 };
|
||||
if w == 0 {
|
||||
continue;
|
||||
}
|
||||
let bar_height = mag * height;
|
||||
let full_cells = bar_height as usize;
|
||||
let frac = bar_height - full_cells as f32;
|
||||
let frac_idx = (frac * 8.0) as usize;
|
||||
|
||||
let x_start = area.x + (band * band_width) as u16;
|
||||
|
||||
for row in 0..area.height as usize {
|
||||
let y = area.y + area.height - 1 - row as u16;
|
||||
let ratio = row as f32 / area.height as f32;
|
||||
@@ -47,11 +51,8 @@ impl Widget for Spectrum<'_> {
|
||||
} else {
|
||||
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
|
||||
};
|
||||
for dx in 0..band_width as u16 {
|
||||
for dx in 0..w as u16 {
|
||||
let x = x_start + dx;
|
||||
if x >= area.x + area.width {
|
||||
break;
|
||||
}
|
||||
if row < full_cells {
|
||||
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
|
||||
} else if row == full_cells && frac_idx > 0 {
|
||||
@@ -59,6 +60,7 @@ impl Widget for Spectrum<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
x_start += w as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1745
src/input.rs
1745
src/input.rs
File diff suppressed because it is too large
Load Diff
189
src/input/engine_page.rs
Normal file
189
src/input/engine_page.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{AudioCommand, SeqCommand};
|
||||
use crate::state::{ConfirmAction, DeviceKind, EngineSection, Modal, SettingKind};
|
||||
|
||||
pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
|
||||
KeyCode::Up => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp),
|
||||
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp),
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingPrev);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Down => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioOutputListDown(count));
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioInputListDown(count));
|
||||
}
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingNext);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::PageUp => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
|
||||
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioOutputPageDown(count));
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioInputPageDown(count));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let cursor = ctx.app.audio.output_list.cursor;
|
||||
if cursor < ctx.app.audio.output_devices.len() {
|
||||
let name = ctx.app.audio.output_devices[cursor].name.clone();
|
||||
ctx.dispatch(AppCommand::SetOutputDevice(name));
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
DeviceKind::Input => {
|
||||
let cursor = ctx.app.audio.input_list.cursor;
|
||||
if cursor < ctx.app.audio.input_devices.len() {
|
||||
let name = ctx.app.audio.input_devices[cursor].name.clone();
|
||||
ctx.dispatch(AppCommand::SetInputDevice(name));
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
|
||||
}
|
||||
EngineSection::Settings => {
|
||||
match ctx.app.audio.setting_kind {
|
||||
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::Channels,
|
||||
delta: -1,
|
||||
}),
|
||||
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::BufferSize,
|
||||
delta: -64,
|
||||
}),
|
||||
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::Polyphony,
|
||||
delta: -1,
|
||||
}),
|
||||
SettingKind::Nudge => {
|
||||
let prev = ctx.nudge_us.load(Ordering::Relaxed);
|
||||
ctx.nudge_us
|
||||
.store((prev - 1000).max(-100_000), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Right => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
|
||||
}
|
||||
EngineSection::Settings => {
|
||||
match ctx.app.audio.setting_kind {
|
||||
SettingKind::Channels => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::Channels,
|
||||
delta: 1,
|
||||
}),
|
||||
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::BufferSize,
|
||||
delta: 64,
|
||||
}),
|
||||
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
|
||||
setting: SettingKind::Polyphony,
|
||||
delta: 1,
|
||||
}),
|
||||
SettingKind::Nudge => {
|
||||
let prev = ctx.nudge_us.load(Ordering::Relaxed);
|
||||
ctx.nudge_us
|
||||
.store((prev + 1000).min(100_000), Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
},
|
||||
KeyCode::Char('R') => ctx.dispatch(AppCommand::AudioTriggerRestart),
|
||||
KeyCode::Char('A') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
let state = FileBrowserState::new_load(String::new());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::AddSamplePath(Box::new(state))));
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
if ctx.app.audio.section == EngineSection::Samples {
|
||||
ctx.dispatch(AppCommand::RemoveLastSamplePath);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::AudioRefreshDevices);
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Found {out_count} output, {in_count} input devices"
|
||||
)));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
||||
KeyCode::Char('t') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
time: None,
|
||||
});
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
114
src/input/help_page.rs
Normal file
114
src/input/help_page.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::state::{ConfirmAction, DictFocus, HelpFocus, Modal};
|
||||
|
||||
pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if ctx.app.ui.help_search_active {
|
||||
match key.code {
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::HelpClearSearch),
|
||||
KeyCode::Enter => ctx.dispatch(AppCommand::HelpSearchConfirm),
|
||||
KeyCode::Backspace => ctx.dispatch(AppCommand::HelpSearchBackspace),
|
||||
KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::HelpSearchInput(c)),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpActivateSearch);
|
||||
}
|
||||
KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => {
|
||||
ctx.dispatch(AppCommand::HelpClearSearch);
|
||||
}
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
|
||||
KeyCode::Char('j') | KeyCode::Down if ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpNextTopic(5));
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up if ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpPrevTopic(5));
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.help_focus {
|
||||
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpNextTopic(1)),
|
||||
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollDown(1)),
|
||||
},
|
||||
KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.help_focus {
|
||||
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpPrevTopic(1)),
|
||||
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollUp(1)),
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if ctx.app.ui.dict_search_active {
|
||||
match key.code {
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::DictClearSearch),
|
||||
KeyCode::Enter => ctx.dispatch(AppCommand::DictSearchConfirm),
|
||||
KeyCode::Backspace => ctx.dispatch(AppCommand::DictSearchBackspace),
|
||||
KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::DictSearchInput(c)),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => {
|
||||
ctx.dispatch(AppCommand::DictActivateSearch);
|
||||
}
|
||||
KeyCode::Esc if !ctx.app.ui.dict_search_query.is_empty() => {
|
||||
ctx.dispatch(AppCommand::DictClearSearch);
|
||||
}
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::DictToggleFocus),
|
||||
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.dict_focus {
|
||||
DictFocus::Categories => ctx.dispatch(AppCommand::DictNextCategory),
|
||||
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollDown(1)),
|
||||
},
|
||||
KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.dict_focus {
|
||||
DictFocus::Categories => ctx.dispatch(AppCommand::DictPrevCategory),
|
||||
DictFocus::Words => ctx.dispatch(AppCommand::DictScrollUp(1)),
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
248
src/input/main_page.rs
Normal file
248
src/input/main_page.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::state::{
|
||||
ConfirmAction, CyclicEnum, EuclideanField, Modal, PanelFocus, PatternField, RenameTarget,
|
||||
SampleBrowserState, SidePanel,
|
||||
};
|
||||
|
||||
pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputResult {
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Tab => {
|
||||
if ctx.app.panel.visible {
|
||||
ctx.app.panel.visible = false;
|
||||
ctx.app.panel.focus = PanelFocus::Main;
|
||||
} else {
|
||||
if ctx.app.panel.side.is_none() {
|
||||
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
|
||||
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
|
||||
}
|
||||
ctx.app.panel.visible = true;
|
||||
ctx.app.panel.focus = PanelFocus::Side;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
KeyCode::Left if shift && !ctrl => {
|
||||
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
|
||||
}
|
||||
ctx.dispatch(AppCommand::PrevStep);
|
||||
}
|
||||
KeyCode::Right if shift && !ctrl => {
|
||||
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
|
||||
}
|
||||
ctx.dispatch(AppCommand::NextStep);
|
||||
}
|
||||
KeyCode::Up if shift && !ctrl => {
|
||||
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
|
||||
}
|
||||
ctx.dispatch(AppCommand::StepUp);
|
||||
}
|
||||
KeyCode::Down if shift && !ctrl => {
|
||||
if ctx.app.editor_ctx.selection_anchor.is_none() {
|
||||
ctx.dispatch(AppCommand::SetSelectionAnchor(ctx.app.editor_ctx.step));
|
||||
}
|
||||
ctx.dispatch(AppCommand::StepDown);
|
||||
}
|
||||
KeyCode::Left => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
ctx.dispatch(AppCommand::PrevStep);
|
||||
}
|
||||
KeyCode::Right => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
ctx.dispatch(AppCommand::NextStep);
|
||||
}
|
||||
KeyCode::Up => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
ctx.dispatch(AppCommand::StepUp);
|
||||
}
|
||||
KeyCode::Down => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
ctx.dispatch(AppCommand::StepDown);
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
ctx.app.editor_ctx.clear_selection();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Editor));
|
||||
}
|
||||
KeyCode::Char('t') => ctx.dispatch(AppCommand::ToggleSteps),
|
||||
KeyCode::Char('s') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
let initial = ctx
|
||||
.app
|
||||
.project_state
|
||||
.file_path
|
||||
.as_ref()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_default();
|
||||
let state = FileBrowserState::new_save(initial);
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
ctx.dispatch(AppCommand::CopySteps);
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
ctx.dispatch(AppCommand::PasteSteps);
|
||||
}
|
||||
KeyCode::Char('b') if ctrl => {
|
||||
ctx.dispatch(AppCommand::LinkPasteSteps);
|
||||
}
|
||||
KeyCode::Char('d') if ctrl => {
|
||||
ctx.dispatch(AppCommand::DuplicateSteps);
|
||||
}
|
||||
KeyCode::Char('h') if ctrl => ctx.dispatch(AppCommand::HardenSteps),
|
||||
KeyCode::Char('l') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
let default_dir = ctx
|
||||
.app
|
||||
.project_state
|
||||
.file_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| {
|
||||
let mut s = p.display().to_string();
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let state = FileBrowserState::new_load(default_dir);
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::FileBrowser(Box::new(state))));
|
||||
}
|
||||
KeyCode::Char('+') | KeyCode::Char('=') => ctx.dispatch(AppCommand::TempoUp),
|
||||
KeyCode::Char('-') => ctx.dispatch(AppCommand::TempoDown),
|
||||
KeyCode::Char('T') => {
|
||||
let current = format!("{:.1}", ctx.link.tempo());
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::SetTempo(current)));
|
||||
}
|
||||
KeyCode::Char('<') | KeyCode::Char(',') => ctx.dispatch(AppCommand::LengthDecrease),
|
||||
KeyCode::Char('>') | KeyCode::Char('.') => ctx.dispatch(AppCommand::LengthIncrease),
|
||||
KeyCode::Char('[') => ctx.dispatch(AppCommand::SpeedDecrease),
|
||||
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
||||
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
||||
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenModal(Modal::Preview)),
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
||||
let steps: Vec<usize> = range.collect();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::DeleteSteps { bank, pattern, steps },
|
||||
selected: false,
|
||||
}));
|
||||
} else {
|
||||
let step = ctx.app.editor_ctx.step;
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::DeleteStep { bank, pattern, step },
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') if ctrl => {
|
||||
let pattern = ctx.app.current_edit_pattern();
|
||||
if let Some(script) = pattern.resolve_script(ctx.app.editor_ctx.step) {
|
||||
if !script.trim().is_empty() {
|
||||
match ctx
|
||||
.app
|
||||
.execute_script_oneshot(script, ctx.link, ctx.audio_tx)
|
||||
{
|
||||
Ok(()) => ctx
|
||||
.app
|
||||
.ui
|
||||
.flash("Executed", 100, crate::state::FlashKind::Info),
|
||||
Err(e) => ctx.app.ui.flash(
|
||||
&format!("Error: {e}"),
|
||||
200,
|
||||
crate::state::FlashKind::Error,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
let (bank, pattern, step) = (
|
||||
ctx.app.editor_ctx.bank,
|
||||
ctx.app.editor_ctx.pattern,
|
||||
ctx.app.editor_ctx.step,
|
||||
);
|
||||
let current_name = ctx
|
||||
.app
|
||||
.current_edit_pattern()
|
||||
.step(step)
|
||||
.and_then(|s| s.name.clone())
|
||||
.unwrap_or_default();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
|
||||
target: RenameTarget::Step { bank, pattern, step },
|
||||
name: current_name,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('o') => {
|
||||
ctx.app.audio.config.layout = ctx.app.audio.config.layout.next();
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
KeyCode::Char('e') | KeyCode::Char('E') => {
|
||||
let (bank, pattern, step) = (
|
||||
ctx.app.editor_ctx.bank,
|
||||
ctx.app.editor_ctx.pattern,
|
||||
ctx.app.editor_ctx.step,
|
||||
);
|
||||
let pattern_len = ctx.app.current_edit_pattern().length;
|
||||
let default_steps = pattern_len.min(32);
|
||||
let default_pulses = (default_steps / 2).max(1).min(default_steps);
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::EuclideanDistribution {
|
||||
bank,
|
||||
pattern,
|
||||
source_step: step,
|
||||
field: EuclideanField::Pulses,
|
||||
pulses: default_pulses.to_string(),
|
||||
steps: default_steps.to_string(),
|
||||
rotation: "0".to_string(),
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('X') => {
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
ctx.dispatch(AppCommand::OpenPreludeEditor);
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
167
src/input/mod.rs
Normal file
167
src/input/mod.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
mod engine_page;
|
||||
mod help_page;
|
||||
mod main_page;
|
||||
mod modal;
|
||||
mod options_page;
|
||||
mod panel;
|
||||
mod patterns_page;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||
use crate::page::Page;
|
||||
use crate::state::{Modal, PanelFocus};
|
||||
|
||||
pub enum InputResult {
|
||||
Continue,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub struct InputContext<'a> {
|
||||
pub app: &'a mut App,
|
||||
pub link: &'a LinkState,
|
||||
pub snapshot: &'a SequencerSnapshot,
|
||||
pub playing: &'a Arc<AtomicBool>,
|
||||
pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
|
||||
pub seq_cmd_tx: &'a Sender<SeqCommand>,
|
||||
pub nudge_us: &'a Arc<AtomicI64>,
|
||||
}
|
||||
|
||||
impl<'a> InputContext<'a> {
|
||||
fn dispatch(&mut self, cmd: AppCommand) {
|
||||
self.app.dispatch(cmd, self.link, self.snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
if handle_live_keys(ctx, &key) {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
if key.kind == KeyEventKind::Release {
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let is_arrow = matches!(
|
||||
key.code,
|
||||
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down
|
||||
);
|
||||
if ctx.app.ui.minimap_until.is_some() && !(ctrl && is_arrow) {
|
||||
ctx.dispatch(AppCommand::ClearMinimap);
|
||||
}
|
||||
|
||||
if ctx.app.ui.show_title {
|
||||
ctx.dispatch(AppCommand::HideTitle);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
ctx.dispatch(AppCommand::ClearStatus);
|
||||
|
||||
if matches!(ctx.app.ui.modal, Modal::None) {
|
||||
handle_normal_input(ctx, key)
|
||||
} else {
|
||||
modal::handle_modal_input(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
|
||||
match (key.code, key.kind) {
|
||||
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
|
||||
(KeyCode::Char('f'), KeyEventKind::Press) => {
|
||||
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
|
||||
return panel::handle_panel_input(ctx, key);
|
||||
}
|
||||
|
||||
if ctrl {
|
||||
let minimap_timeout = Some(Instant::now() + Duration::from_millis(250));
|
||||
match key.code {
|
||||
KeyCode::Left => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageLeft);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageRight);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageUp);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
ctx.app.ui.minimap_until = minimap_timeout;
|
||||
ctx.dispatch(AppCommand::PageDown);
|
||||
return InputResult::Continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match ctx.app.page {
|
||||
Page::Main => main_page::handle_main_page(ctx, key, ctrl),
|
||||
Page::Patterns => patterns_page::handle_patterns_page(ctx, key),
|
||||
Page::Engine => engine_page::handle_engine_page(ctx, key),
|
||||
Page::Options => options_page::handle_options_page(ctx, key),
|
||||
Page::Help => help_page::handle_help_page(ctx, key),
|
||||
Page::Dict => help_page::handle_dict_page(ctx, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_project_samples(ctx: &mut InputContext) {
|
||||
let paths = ctx.app.project_state.project.sample_paths.clone();
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut total_count = 0;
|
||||
let mut all_preload_entries = Vec::new();
|
||||
for path in &paths {
|
||||
if path.is_dir() {
|
||||
let index = doux::sampling::scan_samples_dir(path);
|
||||
let count = index.len();
|
||||
total_count += count;
|
||||
for e in &index {
|
||||
all_preload_entries.push((e.name.clone(), e.path.clone()));
|
||||
}
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
|
||||
}
|
||||
}
|
||||
|
||||
ctx.app.audio.config.sample_paths = paths;
|
||||
ctx.app.audio.config.sample_count = total_count;
|
||||
|
||||
if total_count > 0 {
|
||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||
let sr = ctx.app.audio.config.sample_rate;
|
||||
std::thread::Builder::new()
|
||||
.name("sample-preload".into())
|
||||
.spawn(move || {
|
||||
crate::init::preload_sample_heads(all_preload_entries, sr, ®istry);
|
||||
})
|
||||
.expect("failed to spawn preload thread");
|
||||
}
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Loaded {total_count} samples from project"
|
||||
)));
|
||||
}
|
||||
}
|
||||
553
src/input/modal.rs
Normal file
553
src/input/modal.rs
Normal file
@@ -0,0 +1,553 @@
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::SeqCommand;
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::state::{
|
||||
ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField,
|
||||
PatternPropsField, RenameTarget,
|
||||
};
|
||||
|
||||
pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match &mut ctx.app.ui.modal {
|
||||
Modal::Confirm { action, selected } => {
|
||||
let (action, confirmed) = (action.clone(), *selected);
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => return execute_confirm(ctx, &action),
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
if let Modal::Confirm { selected, .. } = &mut ctx.app.ui.modal {
|
||||
*selected = !*selected;
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if confirmed {
|
||||
return execute_confirm(ctx, &action);
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::FileBrowser(state) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
use crate::state::file_browser::FileBrowserMode;
|
||||
let mode = state.mode.clone();
|
||||
if let Some(path) = state.confirm() {
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
match mode {
|
||||
FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)),
|
||||
FileBrowserMode::Load => {
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState);
|
||||
ctx.dispatch(AppCommand::Load(path));
|
||||
super::load_project_samples(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Tab => state.autocomplete(),
|
||||
KeyCode::Left => state.go_up(),
|
||||
KeyCode::Right => state.enter_selected(),
|
||||
KeyCode::Up => state.select_prev(12),
|
||||
KeyCode::Down => state.select_next(12),
|
||||
KeyCode::Backspace => state.backspace(),
|
||||
KeyCode::Char(c) => {
|
||||
state.input.push(c);
|
||||
state.refresh_entries();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::Rename { target, name } => {
|
||||
let target = target.clone();
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
let new_name = if name.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
ctx.dispatch(rename_command(&target, new_name));
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
if let Modal::Rename { name, .. } = &mut ctx.app.ui.modal {
|
||||
name.pop();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
if let Modal::Rename { name, .. } = &mut ctx.app.ui.modal {
|
||||
name.push(c);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::SetPattern { field, input } => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let field = *field;
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
match field {
|
||||
PatternField::Length => {
|
||||
if let Ok(len) = input.parse::<usize>() {
|
||||
ctx.dispatch(AppCommand::SetLength {
|
||||
bank,
|
||||
pattern,
|
||||
length: len,
|
||||
});
|
||||
let new_len = ctx
|
||||
.app
|
||||
.project_state
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.length;
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Length set to {new_len}")));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus("Invalid length".to_string()));
|
||||
}
|
||||
}
|
||||
PatternField::Speed => {
|
||||
if let Some(speed) = PatternSpeed::from_label(input) {
|
||||
ctx.dispatch(AppCommand::SetSpeed {
|
||||
bank,
|
||||
pattern,
|
||||
speed,
|
||||
});
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Speed set to {}",
|
||||
speed.label()
|
||||
)));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus(
|
||||
"Invalid speed (try 1/3, 2/5, 1x, 2x)".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::SetTempo(input) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
if let Ok(tempo) = input.parse::<f64>() {
|
||||
let tempo = tempo.clamp(20.0, 300.0);
|
||||
ctx.link.set_tempo(tempo);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"Tempo set to {tempo:.1} BPM"
|
||||
)));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus("Invalid tempo".to_string()));
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() || c == '.' => input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Modal::AddSamplePath(state) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let sample_path = if let Some(entry) = state.entries.get(state.selected) {
|
||||
if entry.is_dir && entry.name != ".." {
|
||||
Some(state.current_dir().join(&entry.name))
|
||||
} else if entry.is_dir {
|
||||
state.enter_selected();
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let dir = state.current_dir();
|
||||
if dir.is_dir() {
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(path) = sample_path {
|
||||
let index = doux::sampling::scan_samples_dir(&path);
|
||||
let count = index.len();
|
||||
let preload_entries: Vec<(String, std::path::PathBuf)> = index
|
||||
.iter()
|
||||
.map(|e| (e.name.clone(), e.path.clone()))
|
||||
.collect();
|
||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSamples(index));
|
||||
ctx.app.audio.config.sample_count += count;
|
||||
ctx.app.audio.add_sample_path(path);
|
||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||
let sr = ctx.app.audio.config.sample_rate;
|
||||
std::thread::Builder::new()
|
||||
.name("sample-preload".into())
|
||||
.spawn(move || {
|
||||
crate::init::preload_sample_heads(preload_entries, sr, ®istry);
|
||||
})
|
||||
.expect("failed to spawn preload thread");
|
||||
}
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Tab => state.autocomplete(),
|
||||
KeyCode::Left => state.go_up(),
|
||||
KeyCode::Right => state.enter_selected(),
|
||||
KeyCode::Up => state.select_prev(14),
|
||||
KeyCode::Down => state.select_next(14),
|
||||
KeyCode::Backspace => state.backspace(),
|
||||
KeyCode::Char(c) => {
|
||||
state.input.push(c);
|
||||
state.refresh_entries();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::Editor => {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
let editor = &mut ctx.app.editor_ctx.editor;
|
||||
|
||||
if editor.search_active() {
|
||||
match key.code {
|
||||
KeyCode::Esc => editor.search_clear(),
|
||||
KeyCode::Enter => editor.search_confirm(),
|
||||
KeyCode::Backspace => editor.search_backspace(),
|
||||
KeyCode::Char(c) if !ctrl => editor.search_input(c),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
if editor.sample_finder_active() {
|
||||
match key.code {
|
||||
KeyCode::Esc => editor.dismiss_sample_finder(),
|
||||
KeyCode::Tab | KeyCode::Enter => editor.accept_sample_finder(),
|
||||
KeyCode::Backspace => editor.sample_finder_backspace(),
|
||||
KeyCode::Char('n') if ctrl => editor.sample_finder_next(),
|
||||
KeyCode::Char('p') if ctrl => editor.sample_finder_prev(),
|
||||
KeyCode::Char(c) if !ctrl => editor.sample_finder_input(c),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
if editor.is_selecting() {
|
||||
editor.cancel_selection();
|
||||
} else if editor.completion_active() {
|
||||
editor.dismiss_completion();
|
||||
} else {
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') if ctrl => match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
},
|
||||
KeyCode::Char('b') if ctrl => {
|
||||
editor.activate_sample_finder();
|
||||
}
|
||||
KeyCode::Char('f') if ctrl => {
|
||||
editor.activate_search();
|
||||
}
|
||||
KeyCode::Char('n') if ctrl => {
|
||||
if editor.completion_active() {
|
||||
editor.completion_next();
|
||||
} else if editor.sample_finder_active() {
|
||||
editor.sample_finder_next();
|
||||
} else {
|
||||
editor.search_next();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('p') if ctrl => {
|
||||
if editor.completion_active() {
|
||||
editor.completion_prev();
|
||||
} else if editor.sample_finder_active() {
|
||||
editor.sample_finder_prev();
|
||||
} else {
|
||||
editor.search_prev();
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') if ctrl => {
|
||||
ctx.dispatch(AppCommand::ToggleEditorStack);
|
||||
}
|
||||
KeyCode::Char('r') if ctrl => {
|
||||
let script = ctx.app.editor_ctx.editor.lines().join("\n");
|
||||
match ctx
|
||||
.app
|
||||
.execute_script_oneshot(&script, ctx.link, ctx.audio_tx)
|
||||
{
|
||||
Ok(()) => ctx
|
||||
.app
|
||||
.ui
|
||||
.flash("Executed", 100, crate::state::FlashKind::Info),
|
||||
Err(e) => ctx.app.ui.flash(
|
||||
&format!("Error: {e}"),
|
||||
200,
|
||||
crate::state::FlashKind::Error,
|
||||
),
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') if ctrl => {
|
||||
editor.select_all();
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
editor.copy();
|
||||
}
|
||||
KeyCode::Char('x') if ctrl => {
|
||||
editor.cut();
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
editor.paste();
|
||||
}
|
||||
KeyCode::Left | KeyCode::Right | KeyCode::Up | KeyCode::Down if shift => {
|
||||
if !editor.is_selecting() {
|
||||
editor.start_selection();
|
||||
}
|
||||
editor.input(Event::Key(key));
|
||||
}
|
||||
_ => {
|
||||
editor.input(Event::Key(key));
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.app.editor_ctx.show_stack {
|
||||
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
||||
}
|
||||
}
|
||||
Modal::Preview => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
|
||||
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
|
||||
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
|
||||
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
||||
_ => {}
|
||||
},
|
||||
Modal::PatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
field,
|
||||
name,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
sync_mode,
|
||||
} => {
|
||||
let (bank, pattern) = (*bank, *pattern);
|
||||
match key.code {
|
||||
KeyCode::Up => *field = field.prev(),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||
KeyCode::Left => match field {
|
||||
PatternPropsField::Speed => *speed = speed.prev(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.prev(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Right => match field {
|
||||
PatternPropsField::Speed => *speed = speed.next(),
|
||||
PatternPropsField::Quantization => *quantization = quantization.next(),
|
||||
PatternPropsField::SyncMode => *sync_mode = sync_mode.toggle(),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char(c) => match field {
|
||||
PatternPropsField::Name => name.push(c),
|
||||
PatternPropsField::Length if c.is_ascii_digit() => length.push(c),
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Backspace => match field {
|
||||
PatternPropsField::Name => {
|
||||
name.pop();
|
||||
}
|
||||
PatternPropsField::Length => {
|
||||
length.pop();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
let name_val = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
let sync_val = *sync_mode;
|
||||
ctx.dispatch(AppCommand::StagePatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
name: name_val,
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
sync_mode: sync_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page).len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
*scroll = scroll.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
*scroll = (*scroll + 1).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
*scroll = scroll.saturating_sub(10);
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
*scroll = (*scroll + 10).min(bindings_count.saturating_sub(1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::EuclideanDistribution {
|
||||
bank,
|
||||
pattern,
|
||||
source_step,
|
||||
field,
|
||||
pulses,
|
||||
steps,
|
||||
rotation,
|
||||
} => {
|
||||
let (bank_val, pattern_val, source_step_val) = (*bank, *pattern, *source_step);
|
||||
match key.code {
|
||||
KeyCode::Up => *field = field.prev(),
|
||||
KeyCode::Down | KeyCode::Tab => *field = field.next(),
|
||||
KeyCode::Left => {
|
||||
let target = match field {
|
||||
EuclideanField::Pulses => pulses,
|
||||
EuclideanField::Steps => steps,
|
||||
EuclideanField::Rotation => rotation,
|
||||
};
|
||||
if let Ok(val) = target.parse::<usize>() {
|
||||
*target = val.saturating_sub(1).to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
let target = match field {
|
||||
EuclideanField::Pulses => pulses,
|
||||
EuclideanField::Steps => steps,
|
||||
EuclideanField::Rotation => rotation,
|
||||
};
|
||||
if let Ok(val) = target.parse::<usize>() {
|
||||
*target = (val + 1).min(128).to_string();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if c.is_ascii_digit() => match field {
|
||||
EuclideanField::Pulses => pulses.push(c),
|
||||
EuclideanField::Steps => steps.push(c),
|
||||
EuclideanField::Rotation => rotation.push(c),
|
||||
},
|
||||
KeyCode::Backspace => match field {
|
||||
EuclideanField::Pulses => {
|
||||
pulses.pop();
|
||||
}
|
||||
EuclideanField::Steps => {
|
||||
steps.pop();
|
||||
}
|
||||
EuclideanField::Rotation => {
|
||||
rotation.pop();
|
||||
}
|
||||
},
|
||||
KeyCode::Enter => {
|
||||
let pulses_val: usize = pulses.parse().unwrap_or(0);
|
||||
let steps_val: usize = steps.parse().unwrap_or(0);
|
||||
let rotation_val: usize = rotation.parse().unwrap_or(0);
|
||||
if pulses_val > 0 && steps_val > 0 && pulses_val <= steps_val {
|
||||
ctx.dispatch(AppCommand::ApplyEuclideanDistribution {
|
||||
bank: bank_val,
|
||||
pattern: pattern_val,
|
||||
source_step: source_step_val,
|
||||
pulses: pulses_val,
|
||||
steps: steps_val,
|
||||
rotation: rotation_val,
|
||||
});
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SetStatus(
|
||||
"Invalid: pulses must be > 0 and <= steps".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Modal::None => unreachable!(),
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn execute_confirm(ctx: &mut InputContext, action: &ConfirmAction) -> InputResult {
|
||||
match action {
|
||||
ConfirmAction::Quit => return InputResult::Quit,
|
||||
ConfirmAction::DeleteStep { bank, pattern, step } => {
|
||||
ctx.dispatch(AppCommand::DeleteStep { bank: *bank, pattern: *pattern, step: *step });
|
||||
}
|
||||
ConfirmAction::DeleteSteps { bank, pattern, steps } => {
|
||||
ctx.dispatch(AppCommand::DeleteSteps { bank: *bank, pattern: *pattern, steps: steps.clone() });
|
||||
}
|
||||
ConfirmAction::ResetPattern { bank, pattern } => {
|
||||
ctx.dispatch(AppCommand::ResetPattern { bank: *bank, pattern: *pattern });
|
||||
}
|
||||
ConfirmAction::ResetBank { bank } => {
|
||||
ctx.dispatch(AppCommand::ResetBank { bank: *bank });
|
||||
}
|
||||
ConfirmAction::ResetPatterns { bank, patterns } => {
|
||||
ctx.dispatch(AppCommand::ResetPatterns { bank: *bank, patterns: patterns.clone() });
|
||||
}
|
||||
ConfirmAction::ResetBanks { banks } => {
|
||||
ctx.dispatch(AppCommand::ResetBanks { banks: banks.clone() });
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
InputResult::Continue
|
||||
}
|
||||
|
||||
fn rename_command(target: &RenameTarget, name: Option<String>) -> AppCommand {
|
||||
match target {
|
||||
RenameTarget::Bank { bank } => AppCommand::RenameBank { bank: *bank, name },
|
||||
RenameTarget::Pattern { bank, pattern } => AppCommand::RenamePattern {
|
||||
bank: *bank, pattern: *pattern, name,
|
||||
},
|
||||
RenameTarget::Step { bank, pattern, step } => AppCommand::RenameStep {
|
||||
bank: *bank, pattern: *pattern, step: *step, name,
|
||||
},
|
||||
}
|
||||
}
|
||||
178
src/input/options_page.rs
Normal file
178
src/input/options_page.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::state::{ConfirmAction, Modal, OptionsFocus};
|
||||
|
||||
pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => ctx.dispatch(AppCommand::OptionsNextFocus),
|
||||
KeyCode::Up | KeyCode::BackTab => ctx.dispatch(AppCommand::OptionsPrevFocus),
|
||||
KeyCode::Left | KeyCode::Right => {
|
||||
match ctx.app.options.focus {
|
||||
OptionsFocus::ColorScheme => {
|
||||
let new_scheme = if key.code == KeyCode::Left {
|
||||
ctx.app.ui.color_scheme.prev()
|
||||
} else {
|
||||
ctx.app.ui.color_scheme.next()
|
||||
};
|
||||
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
|
||||
}
|
||||
OptionsFocus::HueRotation => {
|
||||
let delta = if key.code == KeyCode::Left { -5.0 } else { 5.0 };
|
||||
let new_rotation = (ctx.app.ui.hue_rotation + delta).rem_euclid(360.0);
|
||||
ctx.dispatch(AppCommand::SetHueRotation(new_rotation));
|
||||
}
|
||||
OptionsFocus::RefreshRate => ctx.dispatch(AppCommand::ToggleRefreshRate),
|
||||
OptionsFocus::RuntimeHighlight => {
|
||||
ctx.dispatch(AppCommand::ToggleRuntimeHighlight);
|
||||
}
|
||||
OptionsFocus::ShowScope => {
|
||||
ctx.dispatch(AppCommand::ToggleScope);
|
||||
}
|
||||
OptionsFocus::ShowSpectrum => {
|
||||
ctx.dispatch(AppCommand::ToggleSpectrum);
|
||||
}
|
||||
OptionsFocus::ShowCompletion => {
|
||||
ctx.dispatch(AppCommand::ToggleCompletion);
|
||||
}
|
||||
OptionsFocus::LinkEnabled => ctx.link.set_enabled(!ctx.link.is_enabled()),
|
||||
OptionsFocus::StartStopSync => ctx
|
||||
.link
|
||||
.set_start_stop_sync_enabled(!ctx.link.is_start_stop_sync_enabled()),
|
||||
OptionsFocus::Quantum => {
|
||||
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
|
||||
ctx.link.set_quantum(ctx.link.quantum() + delta);
|
||||
}
|
||||
OptionsFocus::MidiOutput0
|
||||
| OptionsFocus::MidiOutput1
|
||||
| OptionsFocus::MidiOutput2
|
||||
| OptionsFocus::MidiOutput3 => {
|
||||
let slot = match ctx.app.options.focus {
|
||||
OptionsFocus::MidiOutput0 => 0,
|
||||
OptionsFocus::MidiOutput1 => 1,
|
||||
OptionsFocus::MidiOutput2 => 2,
|
||||
OptionsFocus::MidiOutput3 => 3,
|
||||
_ => 0,
|
||||
};
|
||||
let all_devices = crate::midi::list_midi_outputs();
|
||||
let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| {
|
||||
ctx.app.midi.selected_outputs[slot] == Some(*idx)
|
||||
|| !ctx
|
||||
.app
|
||||
.midi
|
||||
.selected_outputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(s, sel)| s != slot && *sel == Some(*idx))
|
||||
})
|
||||
.collect();
|
||||
let total_options = available.len() + 1;
|
||||
let current_pos = ctx.app.midi.selected_outputs[slot]
|
||||
.and_then(|idx| available.iter().position(|(i, _)| *i == idx))
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
let new_pos = if key.code == KeyCode::Left {
|
||||
if current_pos == 0 {
|
||||
total_options - 1
|
||||
} else {
|
||||
current_pos - 1
|
||||
}
|
||||
} else {
|
||||
(current_pos + 1) % total_options
|
||||
};
|
||||
if new_pos == 0 {
|
||||
ctx.app.midi.disconnect_output(slot);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"MIDI output {slot}: disconnected"
|
||||
)));
|
||||
} else {
|
||||
let (device_idx, device) = available[new_pos - 1];
|
||||
if ctx.app.midi.connect_output(slot, device_idx).is_ok() {
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"MIDI output {}: {}",
|
||||
slot, device.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
OptionsFocus::MidiInput0
|
||||
| OptionsFocus::MidiInput1
|
||||
| OptionsFocus::MidiInput2
|
||||
| OptionsFocus::MidiInput3 => {
|
||||
let slot = match ctx.app.options.focus {
|
||||
OptionsFocus::MidiInput0 => 0,
|
||||
OptionsFocus::MidiInput1 => 1,
|
||||
OptionsFocus::MidiInput2 => 2,
|
||||
OptionsFocus::MidiInput3 => 3,
|
||||
_ => 0,
|
||||
};
|
||||
let all_devices = crate::midi::list_midi_inputs();
|
||||
let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| {
|
||||
ctx.app.midi.selected_inputs[slot] == Some(*idx)
|
||||
|| !ctx
|
||||
.app
|
||||
.midi
|
||||
.selected_inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(s, sel)| s != slot && *sel == Some(*idx))
|
||||
})
|
||||
.collect();
|
||||
let total_options = available.len() + 1;
|
||||
let current_pos = ctx.app.midi.selected_inputs[slot]
|
||||
.and_then(|idx| available.iter().position(|(i, _)| *i == idx))
|
||||
.map(|p| p + 1)
|
||||
.unwrap_or(0);
|
||||
let new_pos = if key.code == KeyCode::Left {
|
||||
if current_pos == 0 {
|
||||
total_options - 1
|
||||
} else {
|
||||
current_pos - 1
|
||||
}
|
||||
} else {
|
||||
(current_pos + 1) % total_options
|
||||
};
|
||||
if new_pos == 0 {
|
||||
ctx.app.midi.disconnect_input(slot);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"MIDI input {slot}: disconnected"
|
||||
)));
|
||||
} else {
|
||||
let (device_idx, device) = available[new_pos - 1];
|
||||
if ctx.app.midi.connect_input(slot, device_idx).is_ok() {
|
||||
ctx.dispatch(AppCommand::SetStatus(format!(
|
||||
"MIDI input {}: {}",
|
||||
slot, device.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.app.save_settings(ctx.link);
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
95
src/input/panel.rs
Normal file
95
src/input/panel.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::AudioCommand;
|
||||
use crate::state::SidePanel;
|
||||
use cagire_ratatui::TreeLineKind;
|
||||
|
||||
pub(super) fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let state = match &mut ctx.app.panel.side {
|
||||
Some(SidePanel::SampleBrowser(s)) => s,
|
||||
None => return InputResult::Continue,
|
||||
};
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if state.search_active {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
state.clear_search();
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
state.search_query.pop();
|
||||
state.update_search();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
state.search_active = false;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
state.search_query.push(c);
|
||||
state.update_search();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if ctrl {
|
||||
match key.code {
|
||||
KeyCode::Up => {
|
||||
for _ in 0..10 {
|
||||
state.move_up();
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
for _ in 0..10 {
|
||||
state.move_down(30);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
match key.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => state.move_up(),
|
||||
KeyCode::Down | KeyCode::Char('j') => state.move_down(30),
|
||||
KeyCode::PageUp => {
|
||||
for _ in 0..20 {
|
||||
state.move_up();
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
for _ in 0..20 {
|
||||
state.move_down(30);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter | KeyCode::Right => {
|
||||
if let Some(entry) = state.current_entry() {
|
||||
match entry.kind {
|
||||
TreeLineKind::File => {
|
||||
let folder = &entry.folder;
|
||||
let idx = entry.index;
|
||||
let cmd = format!("/sound/{folder}/n/{idx}/gain/1.00/dur/1");
|
||||
let _ = ctx
|
||||
.audio_tx
|
||||
.load()
|
||||
.send(AudioCommand::Evaluate { cmd, time: None });
|
||||
}
|
||||
_ => state.toggle_expand(),
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => state.collapse_at_cursor(),
|
||||
KeyCode::Char('/') => state.activate_search(),
|
||||
KeyCode::Esc => {
|
||||
if state.has_filter() {
|
||||
state.clear_filter();
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::ClosePanel);
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
ctx.dispatch(AppCommand::ClosePanel);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
245
src/input/patterns_page.rs
Normal file
245
src/input/patterns_page.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use super::{InputContext, InputResult};
|
||||
use crate::commands::AppCommand;
|
||||
use crate::state::{ConfirmAction, Modal, PatternsColumn, RenameTarget};
|
||||
|
||||
pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Up if shift => {
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
if ctx.app.patterns_nav.bank_anchor.is_none() {
|
||||
ctx.app.patterns_nav.bank_anchor = Some(ctx.app.patterns_nav.bank_cursor);
|
||||
}
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
if ctx.app.patterns_nav.pattern_anchor.is_none() {
|
||||
ctx.app.patterns_nav.pattern_anchor =
|
||||
Some(ctx.app.patterns_nav.pattern_cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.app.patterns_nav.move_up_clamped();
|
||||
}
|
||||
KeyCode::Down if shift => {
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
if ctx.app.patterns_nav.bank_anchor.is_none() {
|
||||
ctx.app.patterns_nav.bank_anchor = Some(ctx.app.patterns_nav.bank_cursor);
|
||||
}
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
if ctx.app.patterns_nav.pattern_anchor.is_none() {
|
||||
ctx.app.patterns_nav.pattern_anchor =
|
||||
Some(ctx.app.patterns_nav.pattern_cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.app.patterns_nav.move_down_clamped();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
ctx.app.patterns_nav.clear_selection();
|
||||
ctx.dispatch(AppCommand::PatternsCursorUp);
|
||||
}
|
||||
KeyCode::Down => {
|
||||
ctx.app.patterns_nav.clear_selection();
|
||||
ctx.dispatch(AppCommand::PatternsCursorDown);
|
||||
}
|
||||
KeyCode::Left => ctx.dispatch(AppCommand::PatternsCursorLeft),
|
||||
KeyCode::Right => ctx.dispatch(AppCommand::PatternsCursorRight),
|
||||
KeyCode::Esc => {
|
||||
if ctx.app.patterns_nav.has_selection() {
|
||||
ctx.app.patterns_nav.clear_selection();
|
||||
} else if !ctx.app.playback.staged_changes.is_empty()
|
||||
|| !ctx.app.playback.staged_mute_changes.is_empty()
|
||||
|| !ctx.app.playback.staged_prop_changes.is_empty()
|
||||
{
|
||||
ctx.dispatch(AppCommand::ClearStagedChanges);
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::PatternsBack);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if !ctx.app.patterns_nav.has_selection() {
|
||||
ctx.dispatch(AppCommand::PatternsEnter);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
if ctx.app.patterns_nav.column == PatternsColumn::Patterns {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||
ctx.app.stage_pattern_toggle(bank, pattern, ctx.snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
ctx.dispatch(AppCommand::TogglePlaying);
|
||||
ctx.playing
|
||||
.store(ctx.app.playback.playing, Ordering::Relaxed);
|
||||
}
|
||||
KeyCode::Char('c') if !ctrl => {
|
||||
let mute_changed = ctx.app.commit_staged_changes();
|
||||
if mute_changed {
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
KeyCode::Char('c') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
let banks = ctx.app.patterns_nav.selected_banks();
|
||||
if banks.len() > 1 {
|
||||
ctx.dispatch(AppCommand::CopyBanks { banks });
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::CopyBank { bank });
|
||||
}
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let patterns = ctx.app.patterns_nav.selected_patterns();
|
||||
if patterns.len() > 1 {
|
||||
ctx.dispatch(AppCommand::CopyPatterns { bank, patterns });
|
||||
} else {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::CopyPattern { bank, pattern });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('v') if ctrl => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
if ctx.app.copied_banks.as_ref().is_some_and(|v| v.len() > 1) {
|
||||
ctx.dispatch(AppCommand::PasteBanks { start: bank });
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::PasteBank { bank });
|
||||
}
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
if ctx
|
||||
.app
|
||||
.copied_patterns
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > 1)
|
||||
{
|
||||
ctx.dispatch(AppCommand::PastePatterns {
|
||||
bank,
|
||||
start: pattern,
|
||||
});
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::PastePattern { bank, pattern });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
let banks = ctx.app.patterns_nav.selected_banks();
|
||||
if banks.len() > 1 {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::ResetBanks { banks },
|
||||
selected: false,
|
||||
}));
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::ResetBank { bank },
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let patterns = ctx.app.patterns_nav.selected_patterns();
|
||||
if patterns.len() > 1 {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::ResetPatterns { bank, patterns },
|
||||
selected: false,
|
||||
}));
|
||||
} else {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::ResetPattern { bank, pattern },
|
||||
selected: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
if !ctx.app.patterns_nav.has_selection() {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
match ctx.app.patterns_nav.column {
|
||||
PatternsColumn::Banks => {
|
||||
let current_name = ctx.app.project_state.project.banks[bank]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
|
||||
target: RenameTarget::Bank { bank },
|
||||
name: current_name,
|
||||
}));
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
let current_name = ctx.app.project_state.project.banks[bank].patterns
|
||||
[pattern]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
|
||||
target: RenameTarget::Pattern { bank, pattern },
|
||||
name: current_name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') if !ctrl => {
|
||||
if ctx.app.patterns_nav.column == PatternsColumn::Patterns
|
||||
&& !ctx.app.patterns_nav.has_selection()
|
||||
{
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('X') => {
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('?') => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
}
|
||||
@@ -26,24 +26,26 @@ use super::{
|
||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||
};
|
||||
|
||||
fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option<SourceSpan> {
|
||||
let ls = line_start as u32;
|
||||
let ll = line_len as u32;
|
||||
if span.end <= ls || span.start >= ls + ll {
|
||||
return None;
|
||||
}
|
||||
Some(SourceSpan {
|
||||
start: span.start.max(ls) - ls,
|
||||
end: span.end.min(ls + ll) - ls,
|
||||
})
|
||||
}
|
||||
|
||||
fn adjust_spans_for_line(
|
||||
spans: &[SourceSpan],
|
||||
line_start: usize,
|
||||
line_len: usize,
|
||||
) -> Vec<SourceSpan> {
|
||||
let ls = line_start as u32;
|
||||
let ll = line_len as u32;
|
||||
spans
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
if s.end <= ls || s.start >= ls + ll {
|
||||
return None;
|
||||
}
|
||||
Some(SourceSpan {
|
||||
start: s.start.max(ls) - ls,
|
||||
end: (s.end.min(ls + ll)) - ls,
|
||||
})
|
||||
})
|
||||
.filter_map(|s| clip_span(*s, line_start, line_len))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -52,22 +54,9 @@ fn adjust_resolved_for_line(
|
||||
line_start: usize,
|
||||
line_len: usize,
|
||||
) -> Vec<(SourceSpan, String)> {
|
||||
let ls = line_start as u32;
|
||||
let ll = line_len as u32;
|
||||
resolved
|
||||
.iter()
|
||||
.filter_map(|(s, display)| {
|
||||
if s.end <= ls || s.start >= ls + ll {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
SourceSpan {
|
||||
start: s.start.max(ls) - ls,
|
||||
end: (s.end.min(ls + ll)) - ls,
|
||||
},
|
||||
display.clone(),
|
||||
))
|
||||
})
|
||||
.filter_map(|(s, display)| clip_span(*s, line_start, line_len).map(|cs| (cs, display.clone())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -564,252 +553,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.height(18)
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::Preview => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 80 / 100).max(10);
|
||||
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step_idx = app.editor_ctx.step;
|
||||
let step = pattern.step(step_idx);
|
||||
let source_idx = step.and_then(|s| s.source);
|
||||
let step_name = step.and_then(|s| s.name.as_ref());
|
||||
|
||||
let title = match (source_idx, step_name) {
|
||||
(Some(src), Some(name)) => {
|
||||
format!("Step {:02}: {} → {:02}", step_idx + 1, name, src + 1)
|
||||
}
|
||||
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
|
||||
(Some(src), None) => format!("Step {:02} → {:02}", step_idx + 1, src + 1),
|
||||
(None, None) => format!("Step {:02}", step_idx + 1),
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.preview)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
||||
if script.is_empty() {
|
||||
let empty = Paragraph::new("(empty)")
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::new().fg(theme.ui.text_dim));
|
||||
let centered_area = Rect {
|
||||
y: inner.y + inner.height / 2,
|
||||
height: 1,
|
||||
..inner
|
||||
};
|
||||
frame.render_widget(empty, centered_area);
|
||||
} else {
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
let source = pattern.resolve_source(step_idx);
|
||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.map(|t| {
|
||||
t.resolved
|
||||
.iter()
|
||||
.map(|(s, v)| (*s, v.display()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut line_start = 0usize;
|
||||
let lines: Vec<Line> = script
|
||||
.lines()
|
||||
.map(|line_str| {
|
||||
let tokens = if let Some(t) = trace {
|
||||
let exec = adjust_spans_for_line(
|
||||
&t.executed_spans,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
let sel = adjust_spans_for_line(
|
||||
&t.selected_spans,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
let res = adjust_resolved_for_line(
|
||||
&resolved_display,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
highlight_line_with_runtime(line_str, &exec, &sel, &res, &user_words)
|
||||
} else {
|
||||
highlight_line_with_runtime(line_str, &[], &[], &[], &user_words)
|
||||
};
|
||||
line_start += line_str.len() + 1;
|
||||
let spans: Vec<Span> = tokens
|
||||
.into_iter()
|
||||
.map(|(style, text, _)| Span::styled(text, style))
|
||||
.collect();
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let paragraph = Paragraph::new(lines);
|
||||
frame.render_widget(paragraph, inner);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::Editor => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 60 / 100).max(10);
|
||||
|
||||
let flash_kind = app.ui.flash_kind();
|
||||
let border_color = match flash_kind {
|
||||
Some(FlashKind::Error) => theme.flash.error_fg,
|
||||
Some(FlashKind::Info) => theme.ui.text_primary,
|
||||
Some(FlashKind::Success) => theme.flash.success_fg,
|
||||
None => theme.modal.editor,
|
||||
};
|
||||
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
format!("Step {step_num:02}: {name}")
|
||||
} else {
|
||||
format!("Step {step_num:02} Script")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let trace = if app.ui.runtime_highlight
|
||||
&& app.playback.playing
|
||||
&& app.editor_ctx.target == EditorTarget::Step
|
||||
{
|
||||
let source = app
|
||||
.current_edit_pattern()
|
||||
.resolve_source(app.editor_ctx.step);
|
||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text_lines = app.editor_ctx.editor.lines();
|
||||
let mut line_offsets: Vec<usize> = Vec::with_capacity(text_lines.len());
|
||||
let mut offset = 0;
|
||||
for line in text_lines.iter() {
|
||||
line_offsets.push(offset);
|
||||
offset += line.len() + 1;
|
||||
}
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.map(|t| {
|
||||
t.resolved
|
||||
.iter()
|
||||
.map(|(s, v)| (*s, v.display()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> {
|
||||
let line_start = line_offsets[row];
|
||||
let (exec, sel, res) = match trace {
|
||||
Some(t) => (
|
||||
adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
|
||||
adjust_spans_for_line(&t.selected_spans, line_start, line.len()),
|
||||
adjust_resolved_for_line(&resolved_display, line_start, line.len()),
|
||||
),
|
||||
None => (Vec::new(), Vec::new(), Vec::new()),
|
||||
};
|
||||
highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words)
|
||||
};
|
||||
|
||||
let show_search = app.editor_ctx.editor.search_active()
|
||||
|| !app.editor_ctx.editor.search_query().is_empty();
|
||||
|
||||
let reserved_lines = 1 + if show_search { 1 } else { 0 };
|
||||
let editor_height = inner.height.saturating_sub(reserved_lines);
|
||||
|
||||
let mut y = inner.y;
|
||||
|
||||
let search_area = if show_search {
|
||||
let area = Rect::new(inner.x, y, inner.width, 1);
|
||||
y += 1;
|
||||
Some(area)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let editor_area = Rect::new(inner.x, y, inner.width, editor_height);
|
||||
y += editor_height;
|
||||
|
||||
let hint_area = Rect::new(inner.x, y, inner.width, 1);
|
||||
|
||||
if let Some(sa) = search_area {
|
||||
render_search_bar(
|
||||
frame,
|
||||
sa,
|
||||
app.editor_ctx.editor.search_query(),
|
||||
app.editor_ctx.editor.search_active(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(kind) = flash_kind {
|
||||
let bg = match kind {
|
||||
FlashKind::Error => theme.flash.error_bg,
|
||||
FlashKind::Info => theme.flash.info_bg,
|
||||
FlashKind::Success => theme.flash.success_bg,
|
||||
};
|
||||
let flash_block = Block::default().style(Style::default().bg(bg));
|
||||
frame.render_widget(flash_block, editor_area);
|
||||
}
|
||||
app.editor_ctx
|
||||
.editor
|
||||
.render(frame, editor_area, &highlighter);
|
||||
|
||||
if app.editor_ctx.editor.search_active() {
|
||||
let hints = hint_line(&[("Enter", "confirm"), ("Esc", "cancel")]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
} else if app.editor_ctx.show_stack {
|
||||
let stack_text = app
|
||||
.editor_ctx
|
||||
.stack_cache
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|c| c.result.clone())
|
||||
.unwrap_or_else(|| "Stack: []".to_string());
|
||||
let hints = hint_line(&[("Esc", "save"), ("C-e", "eval"), ("C-s", "hide")]);
|
||||
let [hint_left, stack_right] = Layout::horizontal([
|
||||
Constraint::Length(hints.width() as u16),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(hint_area);
|
||||
frame.render_widget(Paragraph::new(hints), hint_left);
|
||||
let dim = Style::default().fg(theme.hint.text);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
|
||||
stack_right,
|
||||
);
|
||||
} else {
|
||||
let hints = hint_line(&[
|
||||
("Esc", "save"),
|
||||
("C-e", "eval"),
|
||||
("C-f", "find"),
|
||||
("C-b", "samples"),
|
||||
("C-s", "stack"),
|
||||
("C-u", "/"),
|
||||
("C-r", "undo/redo"),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::Preview => render_modal_preview(frame, app, snapshot, &user_words, term),
|
||||
Modal::Editor => render_modal_editor(frame, app, snapshot, &user_words, term),
|
||||
Modal::PatternProps {
|
||||
bank,
|
||||
pattern,
|
||||
@@ -846,72 +591,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
let height = (term.height * 80 / 100).max(15);
|
||||
|
||||
let title = format!("Keybindings — {}", app.page.name());
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(*scroll)
|
||||
.take(visible_rows)
|
||||
.map(|(i, (key, name, desc))| {
|
||||
let bg = if i % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
|
||||
Cell::from(*name).style(Style::default().fg(theme.modal.input)),
|
||||
Cell::from(*desc).style(Style::default().fg(theme.ui.text_primary)),
|
||||
])
|
||||
.style(Style::default().bg(bg))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(12),
|
||||
Constraint::Fill(1),
|
||||
],
|
||||
)
|
||||
.column_spacing(2);
|
||||
|
||||
let table_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y,
|
||||
width: inner.width,
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
frame.render_widget(table, table_area);
|
||||
|
||||
let hint_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y + inner.height.saturating_sub(1),
|
||||
width: inner.width,
|
||||
height: 1,
|
||||
};
|
||||
let hints = hint_line(&[("↑↓", "scroll"), ("PgUp/Dn", "page"), ("Esc/?", "close")]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(hints).alignment(Alignment::Right),
|
||||
hint_area,
|
||||
);
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => render_modal_keybindings(frame, app, *scroll, term),
|
||||
Modal::EuclideanDistribution {
|
||||
source_step,
|
||||
field,
|
||||
@@ -969,6 +649,336 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
))
|
||||
}
|
||||
|
||||
fn render_modal_preview(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
snapshot: &SequencerSnapshot,
|
||||
user_words: &HashSet<String>,
|
||||
term: Rect,
|
||||
) -> Rect {
|
||||
let theme = theme::get();
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 80 / 100).max(10);
|
||||
|
||||
let pattern = app.current_edit_pattern();
|
||||
let step_idx = app.editor_ctx.step;
|
||||
let step = pattern.step(step_idx);
|
||||
let source_idx = step.and_then(|s| s.source);
|
||||
let step_name = step.and_then(|s| s.name.as_ref());
|
||||
|
||||
let title = match (source_idx, step_name) {
|
||||
(Some(src), Some(name)) => {
|
||||
format!("Step {:02}: {} → {:02}", step_idx + 1, name, src + 1)
|
||||
}
|
||||
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
|
||||
(Some(src), None) => format!("Step {:02} → {:02}", step_idx + 1, src + 1),
|
||||
(None, None) => format!("Step {:02}", step_idx + 1),
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.preview)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
||||
if script.is_empty() {
|
||||
let empty = Paragraph::new("(empty)")
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::new().fg(theme.ui.text_dim));
|
||||
let centered_area = Rect {
|
||||
y: inner.y + inner.height / 2,
|
||||
height: 1,
|
||||
..inner
|
||||
};
|
||||
frame.render_widget(empty, centered_area);
|
||||
} else {
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
let source = pattern.resolve_source(step_idx);
|
||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.map(|t| {
|
||||
t.resolved
|
||||
.iter()
|
||||
.map(|(s, v)| (*s, v.display()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut line_start = 0usize;
|
||||
let lines: Vec<Line> = script
|
||||
.lines()
|
||||
.map(|line_str| {
|
||||
let tokens = if let Some(t) = trace {
|
||||
let exec = adjust_spans_for_line(
|
||||
&t.executed_spans,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
let sel = adjust_spans_for_line(
|
||||
&t.selected_spans,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
let res = adjust_resolved_for_line(
|
||||
&resolved_display,
|
||||
line_start,
|
||||
line_str.len(),
|
||||
);
|
||||
highlight_line_with_runtime(line_str, &exec, &sel, &res, user_words)
|
||||
} else {
|
||||
highlight_line_with_runtime(line_str, &[], &[], &[], user_words)
|
||||
};
|
||||
line_start += line_str.len() + 1;
|
||||
let spans: Vec<Span> = tokens
|
||||
.into_iter()
|
||||
.map(|(style, text, _)| Span::styled(text, style))
|
||||
.collect();
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let paragraph = Paragraph::new(lines);
|
||||
frame.render_widget(paragraph, inner);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_modal_editor(
|
||||
frame: &mut Frame,
|
||||
app: &App,
|
||||
snapshot: &SequencerSnapshot,
|
||||
user_words: &HashSet<String>,
|
||||
term: Rect,
|
||||
) -> Rect {
|
||||
let theme = theme::get();
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 60 / 100).max(10);
|
||||
|
||||
let flash_kind = app.ui.flash_kind();
|
||||
let border_color = match flash_kind {
|
||||
Some(FlashKind::Error) => theme.flash.error_fg,
|
||||
Some(FlashKind::Info) => theme.ui.text_primary,
|
||||
Some(FlashKind::Success) => theme.flash.success_fg,
|
||||
None => theme.modal.editor,
|
||||
};
|
||||
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
format!("Step {step_num:02}: {name}")
|
||||
} else {
|
||||
format!("Step {step_num:02} Script")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let trace = if app.ui.runtime_highlight
|
||||
&& app.playback.playing
|
||||
&& app.editor_ctx.target == EditorTarget::Step
|
||||
{
|
||||
let source = app
|
||||
.current_edit_pattern()
|
||||
.resolve_source(app.editor_ctx.step);
|
||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text_lines = app.editor_ctx.editor.lines();
|
||||
let mut line_offsets: Vec<usize> = Vec::with_capacity(text_lines.len());
|
||||
let mut offset = 0;
|
||||
for line in text_lines.iter() {
|
||||
line_offsets.push(offset);
|
||||
offset += line.len() + 1;
|
||||
}
|
||||
|
||||
let resolved_display: Vec<(SourceSpan, String)> = trace
|
||||
.map(|t| {
|
||||
t.resolved
|
||||
.iter()
|
||||
.map(|(s, v)| (*s, v.display()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> {
|
||||
let line_start = line_offsets[row];
|
||||
let (exec, sel, res) = match trace {
|
||||
Some(t) => (
|
||||
adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
|
||||
adjust_spans_for_line(&t.selected_spans, line_start, line.len()),
|
||||
adjust_resolved_for_line(&resolved_display, line_start, line.len()),
|
||||
),
|
||||
None => (Vec::new(), Vec::new(), Vec::new()),
|
||||
};
|
||||
highlight::highlight_line_with_runtime(line, &exec, &sel, &res, user_words)
|
||||
};
|
||||
|
||||
let show_search = app.editor_ctx.editor.search_active()
|
||||
|| !app.editor_ctx.editor.search_query().is_empty();
|
||||
|
||||
let reserved_lines = 1 + if show_search { 1 } else { 0 };
|
||||
let editor_height = inner.height.saturating_sub(reserved_lines);
|
||||
|
||||
let mut y = inner.y;
|
||||
|
||||
let search_area = if show_search {
|
||||
let area = Rect::new(inner.x, y, inner.width, 1);
|
||||
y += 1;
|
||||
Some(area)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let editor_area = Rect::new(inner.x, y, inner.width, editor_height);
|
||||
y += editor_height;
|
||||
|
||||
let hint_area = Rect::new(inner.x, y, inner.width, 1);
|
||||
|
||||
if let Some(sa) = search_area {
|
||||
render_search_bar(
|
||||
frame,
|
||||
sa,
|
||||
app.editor_ctx.editor.search_query(),
|
||||
app.editor_ctx.editor.search_active(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(kind) = flash_kind {
|
||||
let bg = match kind {
|
||||
FlashKind::Error => theme.flash.error_bg,
|
||||
FlashKind::Info => theme.flash.info_bg,
|
||||
FlashKind::Success => theme.flash.success_bg,
|
||||
};
|
||||
let flash_block = Block::default().style(Style::default().bg(bg));
|
||||
frame.render_widget(flash_block, editor_area);
|
||||
}
|
||||
app.editor_ctx
|
||||
.editor
|
||||
.render(frame, editor_area, &highlighter);
|
||||
|
||||
if app.editor_ctx.editor.search_active() {
|
||||
let hints = hint_line(&[("Enter", "confirm"), ("Esc", "cancel")]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
} else if app.editor_ctx.show_stack {
|
||||
let stack_text = app
|
||||
.editor_ctx
|
||||
.stack_cache
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.map(|c| c.result.clone())
|
||||
.unwrap_or_else(|| "Stack: []".to_string());
|
||||
let hints = hint_line(&[("Esc", "save"), ("C-e", "eval"), ("C-s", "hide")]);
|
||||
let [hint_left, stack_right] = Layout::horizontal([
|
||||
Constraint::Length(hints.width() as u16),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(hint_area);
|
||||
frame.render_widget(Paragraph::new(hints), hint_left);
|
||||
let dim = Style::default().fg(theme.hint.text);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right),
|
||||
stack_right,
|
||||
);
|
||||
} else {
|
||||
let hints = hint_line(&[
|
||||
("Esc", "save"),
|
||||
("C-e", "eval"),
|
||||
("C-f", "find"),
|
||||
("C-b", "samples"),
|
||||
("C-s", "stack"),
|
||||
("C-u", "/"),
|
||||
("C-r", "undo/redo"),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: Rect) -> Rect {
|
||||
let theme = theme::get();
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
let height = (term.height * 80 / 100).max(15);
|
||||
|
||||
let title = format!("Keybindings — {}", app.page.name());
|
||||
let inner = ModalFrame::new(&title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(scroll)
|
||||
.take(visible_rows)
|
||||
.map(|(i, (key, name, desc))| {
|
||||
let bg = if i % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(*key).style(Style::default().fg(theme.modal.confirm)),
|
||||
Cell::from(*name).style(Style::default().fg(theme.modal.input)),
|
||||
Cell::from(*desc).style(Style::default().fg(theme.ui.text_primary)),
|
||||
])
|
||||
.style(Style::default().bg(bg))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[
|
||||
Constraint::Length(14),
|
||||
Constraint::Length(12),
|
||||
Constraint::Fill(1),
|
||||
],
|
||||
)
|
||||
.column_spacing(2);
|
||||
|
||||
let table_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y,
|
||||
width: inner.width,
|
||||
height: inner.height.saturating_sub(1),
|
||||
};
|
||||
frame.render_widget(table, table_area);
|
||||
|
||||
let hint_area = Rect {
|
||||
x: inner.x,
|
||||
y: inner.y + inner.height.saturating_sub(1),
|
||||
width: inner.width,
|
||||
height: 1,
|
||||
};
|
||||
let hints = hint_line(&[("↑↓", "scroll"), ("PgUp/Dn", "page"), ("Esc/?", "close")]);
|
||||
frame.render_widget(
|
||||
Paragraph::new(hints).alignment(Alignment::Right),
|
||||
hint_area,
|
||||
);
|
||||
|
||||
inner
|
||||
}
|
||||
|
||||
fn format_euclidean_preview(pulses: usize, steps: usize, rotation: usize) -> String {
|
||||
if pulses == 0 || steps == 0 || pulses > steps {
|
||||
return "[invalid]".to_string();
|
||||
|
||||
Reference in New Issue
Block a user