Feat: entretien de la codebase

This commit is contained in:
2026-02-09 21:12:49 +01:00
parent a5f17687f1
commit 869d3af244
25 changed files with 847 additions and 878 deletions

View File

@@ -98,6 +98,7 @@ pub enum Op {
ClearCmd,
SetSpeed,
At,
Arp,
IntRange,
StepRange,
Generate,

View File

@@ -88,6 +88,7 @@ pub enum Value {
Str(Arc<str>, Option<SourceSpan>),
Quotation(Arc<[Op]>, Option<SourceSpan>),
CycleList(Arc<[Value]>),
ArpList(Arc<[Value]>),
}
impl PartialEq for Value {
@@ -98,6 +99,7 @@ impl PartialEq for Value {
(Value::Str(a, _), Value::Str(b, _)) => a == b,
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
(Value::CycleList(a), Value::CycleList(b)) => a == b,
(Value::ArpList(a), Value::ArpList(b)) => a == b,
_ => false,
}
}
@@ -133,7 +135,7 @@ impl Value {
Value::Float(f, _) => *f != 0.0,
Value::Str(s, _) => !s.is_empty(),
Value::Quotation(..) => true,
Value::CycleList(items) => !items.is_empty(),
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
}
}
@@ -143,14 +145,14 @@ impl Value {
Value::Float(f, _) => f.to_string(),
Value::Str(s, _) => s.to_string(),
Value::Quotation(..) => String::new(),
Value::CycleList(_) => String::new(),
Value::CycleList(_) | Value::ArpList(_) => String::new(),
}
}
pub(super) fn span(&self) -> Option<SourceSpan> {
match self {
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
Value::CycleList(_) => None,
Value::CycleList(_) | Value::ArpList(_) => None,
}
}
}

View File

@@ -216,6 +216,28 @@ impl Forth {
sound_len.max(param_max)
};
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
let sound_len = match cmd.sound() {
Some(Value::ArpList(items)) => items.len(),
_ => 0,
};
let param_max = cmd
.params()
.iter()
.map(|(_, v)| match v {
Value::ArpList(items) => items.len(),
_ => 0,
})
.max()
.unwrap_or(0);
sound_len.max(param_max).max(1)
};
let emit_with_cycling = |cmd: &CmdRegister,
emit_idx: usize,
delta_secs: f64,
@@ -231,7 +253,7 @@ impl Forth {
.iter()
.map(|(k, v)| {
let resolved = resolve_cycling(v, emit_idx);
if let Value::CycleList(_) = v {
if let Value::CycleList(_) | Value::ArpList(_) = v {
if let Some(span) = resolved.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
@@ -559,24 +581,36 @@ impl Forth {
}
Op::Emit => {
let poly_count = compute_poly_count(cmd);
let deltas = if cmd.deltas().is_empty() {
vec![Value::Float(0.0, None)]
} else {
cmd.deltas().to_vec()
};
if has_arp_list(cmd) {
let arp_count = compute_arp_count(cmd);
let explicit_deltas = !cmd.deltas().is_empty();
let delta_list: Vec<Value> = if explicit_deltas {
cmd.deltas().to_vec()
} else {
Vec::new()
};
let count = if explicit_deltas {
arp_count.max(delta_list.len())
} else {
arp_count
};
for poly_idx in 0..poly_count {
for delta_val in deltas.iter() {
let delta_frac = delta_val.as_float()?;
let delta_secs = ctx.nudge_secs + delta_frac * ctx.step_duration();
if let Some(span) = delta_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
for i in 0..count {
let delta_secs = if explicit_deltas {
let dv = &delta_list[i % delta_list.len()];
let frac = dv.as_float()?;
if let Some(span) = dv.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
}
ctx.nudge_secs + frac * ctx.step_duration()
} else {
ctx.nudge_secs
+ (i as f64 / count as f64) * ctx.step_duration()
};
if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
emit_with_cycling(cmd, i, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
@@ -585,6 +619,37 @@ impl Forth {
}
}
}
} else {
let poly_count = compute_poly_count(cmd);
let deltas = if cmd.deltas().is_empty() {
vec![Value::Float(0.0, None)]
} else {
cmd.deltas().to_vec()
};
for poly_idx in 0..poly_count {
for delta_val in deltas.iter() {
let delta_frac = delta_val.as_float()?;
let delta_secs =
ctx.nudge_secs + delta_frac * ctx.step_duration();
if let Some(span) = delta_val.span() {
if let Some(trace) = trace_cell.borrow_mut().as_mut() {
trace.selected_spans.push(span);
}
}
if let Some(sound_val) =
emit_with_cycling(cmd, poly_idx, delta_secs, outputs)?
{
if let Some(span) = sound_val.span() {
if let Some(trace) =
trace_cell.borrow_mut().as_mut()
{
trace.selected_spans.push(span);
}
}
}
}
}
}
}
@@ -976,6 +1041,14 @@ impl Forth {
cmd.set_deltas(deltas);
}
Op::Arp => {
if stack.is_empty() {
return Err("stack underflow".into());
}
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")?;
@@ -1528,7 +1601,7 @@ where
fn resolve_cycling(val: &Value, emit_idx: usize) -> Cow<'_, Value> {
match val {
Value::CycleList(items) if !items.is_empty() => {
Value::CycleList(items) | Value::ArpList(items) if !items.is_empty() => {
Cow::Owned(items[emit_idx % items.len()].clone())
}
other => Cow::Borrowed(other),

View File

@@ -79,6 +79,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"tempo!" => Op::SetTempo,
"speed!" => Op::SetSpeed,
"at" => Op::At,
"arp" => Op::Arp,
"adsr" => Op::Adsr,
"ad" => Op::Ad,
"apply" => Op::Apply,

View File

@@ -23,6 +23,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "arp",
aliases: &[],
category: "Sound",
stack: "(v1..vn -- arplist)",
desc: "Wrap stack values as arpeggio list for spreading across deltas",
example: "c4 e4 g4 b4 arp note => arpeggio",
compile: Simple,
varargs: true,
},
Word {
name: "clear",
aliases: &[],

View File

@@ -0,0 +1,145 @@
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, List, ListItem};
use ratatui::Frame;
use crate::theme;
pub struct CategoryItem<'a> {
pub label: &'a str,
pub is_section: bool,
}
pub struct CategoryList<'a> {
items: &'a [CategoryItem<'a>],
selected: usize,
focused: bool,
title: &'a str,
section_color: Color,
focused_color: Color,
selected_color: Color,
normal_color: Color,
dimmed_color: Option<Color>,
}
impl<'a> CategoryList<'a> {
pub fn new(items: &'a [CategoryItem<'a>], selected: usize) -> Self {
let theme = theme::get();
Self {
items,
selected,
focused: false,
title: "",
section_color: theme.ui.text_dim,
focused_color: theme.dict.category_focused,
selected_color: theme.dict.category_selected,
normal_color: theme.dict.category_normal,
dimmed_color: None,
}
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn title(mut self, title: &'a str) -> Self {
self.title = title;
self
}
pub fn selected_color(mut self, color: Color) -> Self {
self.selected_color = color;
self
}
pub fn normal_color(mut self, color: Color) -> Self {
self.normal_color = color;
self
}
pub fn dimmed(mut self, color: Color) -> Self {
self.dimmed_color = Some(color);
self
}
pub fn render(self, frame: &mut Frame, area: Rect) {
let theme = theme::get();
let visible_height = area.height.saturating_sub(2) as usize;
let total_items = self.items.len();
let selected_visual_idx = {
let mut visual = 0;
let mut selectable_count = 0;
for item in self.items.iter() {
if !item.is_section {
if selectable_count == self.selected {
break;
}
selectable_count += 1;
}
visual += 1;
}
visual
};
let scroll = if selected_visual_idx < visible_height / 2 {
0
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
total_items.saturating_sub(visible_height)
} else {
selected_visual_idx.saturating_sub(visible_height / 2)
};
let mut selectable_idx = self.items
.iter()
.take(scroll)
.filter(|e| !e.is_section)
.count();
let is_dimmed = self.dimmed_color.is_some();
let items: Vec<ListItem> = self.items
.iter()
.skip(scroll)
.take(visible_height)
.map(|item| {
if item.is_section {
let style = Style::new().fg(self.section_color);
ListItem::new(format!("{}", item.label)).style(style)
} else {
let is_selected = selectable_idx == self.selected;
let style = if let Some(dim_color) = self.dimmed_color {
Style::new().fg(dim_color)
} else if is_selected && self.focused {
Style::new()
.fg(self.focused_color)
.add_modifier(Modifier::BOLD)
} else if is_selected {
Style::new()
.fg(self.selected_color)
.add_modifier(Modifier::BOLD)
} else {
Style::new().fg(self.normal_color)
};
let prefix = if is_selected && !is_dimmed { "> " } else { " " };
selectable_idx += 1;
ListItem::new(format!("{prefix}{}", item.label)).style(style)
}
})
.collect();
let border_color = if self.focused {
theme.dict.border_focused
} else {
theme.dict.border_normal
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(border_color))
.title(self.title);
let list = List::new(items).block(block);
frame.render_widget(list, area);
}
}

View File

@@ -0,0 +1,27 @@
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use crate::theme;
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
let theme = theme::get();
let key_style = Style::default().fg(theme.hint.key);
let text_style = Style::default().fg(theme.hint.text);
let spans: Vec<Span> = pairs
.iter()
.enumerate()
.flat_map(|(i, (key, action))| {
let mut s = vec![
Span::styled(key.to_string(), key_style),
Span::styled(format!(" {action}"), text_style),
];
if i + 1 < pairs.len() {
s.push(Span::styled(" ", text_style));
}
s
})
.collect();
Line::from(spans)
}

View File

@@ -1,12 +1,18 @@
mod active_patterns;
mod category_list;
mod confirm;
mod editor;
mod file_browser;
mod hint_bar;
mod list_select;
mod modal;
mod nav_minimap;
mod props_form;
mod sample_browser;
mod scope;
mod scroll_indicators;
mod search_bar;
mod section_header;
mod sparkles;
mod spectrum;
mod text_input;
@@ -15,14 +21,20 @@ mod vu_meter;
mod waveform;
pub use active_patterns::{ActivePatterns, MuteStatus};
pub use category_list::{CategoryItem, CategoryList};
pub use confirm::ConfirmModal;
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
pub use file_browser::FileBrowserModal;
pub use hint_bar::hint_line;
pub use list_select::ListSelect;
pub use modal::ModalFrame;
pub use nav_minimap::{NavMinimap, NavTile};
pub use props_form::render_props_form;
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
pub use scope::{Orientation, Scope};
pub use scroll_indicators::{render_scroll_indicators, IndicatorAlign};
pub use search_bar::render_search_bar;
pub use section_header::render_section_header;
pub use sparkles::Sparkles;
pub use spectrum::Spectrum;
pub use text_input::TextInputModal;

View File

@@ -0,0 +1,42 @@
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::theme;
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
let theme = theme::get();
for (i, (label, value, selected)) in fields.iter().enumerate() {
let y = area.y + i as u16;
if y >= area.y + area.height {
break;
}
let (label_style, value_style) = if *selected {
(
Style::default()
.fg(theme.hint.key)
.add_modifier(Modifier::BOLD),
Style::default()
.fg(theme.ui.text_primary)
.bg(theme.ui.surface),
)
} else {
(
Style::default().fg(theme.ui.text_muted),
Style::default().fg(theme.ui.text_primary),
)
};
let label_area = Rect::new(area.x + 1, y, 14, 1);
let value_area = Rect::new(area.x + 16, y, area.width.saturating_sub(18), 1);
frame.render_widget(
Paragraph::new(format!("{label}:")).style(label_style),
label_area,
);
frame.render_widget(Paragraph::new(*value).style(value_style), value_area);
}
}

View File

@@ -0,0 +1,53 @@
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
pub enum IndicatorAlign {
Center,
Right,
}
pub fn render_scroll_indicators(
frame: &mut Frame,
area: Rect,
offset: usize,
visible: usize,
total: usize,
color: Color,
align: IndicatorAlign,
) {
let style = Style::new().fg(color);
match align {
IndicatorAlign::Center => {
if offset > 0 {
let indicator = Paragraph::new("")
.style(style)
.alignment(ratatui::layout::Alignment::Center);
frame.render_widget(indicator, Rect { height: 1, ..area });
}
if offset + visible < total {
let y = area.y + area.height.saturating_sub(1);
let indicator = Paragraph::new("")
.style(style)
.alignment(ratatui::layout::Alignment::Center);
frame.render_widget(indicator, Rect { y, height: 1, ..area });
}
}
IndicatorAlign::Right => {
let x = area.x + area.width.saturating_sub(1);
if offset > 0 {
let indicator = Paragraph::new("").style(style);
frame.render_widget(indicator, Rect::new(x, area.y, 1, 1));
}
if offset + visible < total {
let indicator = Paragraph::new("").style(style);
frame.render_widget(
indicator,
Rect::new(x, area.y + area.height.saturating_sub(1), 1, 1),
);
}
}
}
}

View File

@@ -0,0 +1,20 @@
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::theme;
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
let theme = theme::get();
let style = if active {
Style::new().fg(theme.search.active)
} else {
Style::new().fg(theme.search.inactive)
};
let cursor = if active { "_" } else { "" };
let text = format!(" /{query}{cursor}");
let line = Line::from(Span::styled(text, style));
frame.render_widget(Paragraph::new(vec![line]), area);
}

View File

@@ -0,0 +1,30 @@
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::theme;
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
let theme = theme::get();
let [header_area, divider_area] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
let header_style = if focused {
Style::new()
.fg(theme.engine.header_focused)
.add_modifier(Modifier::BOLD)
} else {
Style::new()
.fg(theme.engine.header)
.add_modifier(Modifier::BOLD)
};
frame.render_widget(Paragraph::new(title).style(header_style), header_area);
let divider = "".repeat(area.width as usize);
frame.render_widget(
Paragraph::new(divider).style(Style::new().fg(theme.engine.divider)),
divider_area,
);
}