From aac9524316ea7b42f3090f11d2aaba5651ceeede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 30 Jan 2026 11:58:16 +0100 Subject: [PATCH] Feat: ability to rename steps --- Cargo.toml | 2 +- crates/project/src/project.rs | 9 +++++- src/app.rs | 13 +++++++++ src/commands.rs | 6 ++++ src/input.rs | 45 +++++++++++++++++++++++++++++ src/state/editor.rs | 1 + src/state/modal.rs | 6 ++++ src/views/main_view.rs | 54 +++++++++++++++++++++++++++++++---- src/views/render.rs | 25 ++++++++++++---- 9 files changed, 149 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30a1aa9..df220f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" cagire-forth = { path = "crates/forth" } cagire-project = { path = "crates/project" } cagire-ratatui = { path = "crates/ratatui" } -doux = { path = "/Users/bubo/doux", features = ["native"] } +doux = { git = "https://github.com/sova-org/doux", features = ["native"] } rusty_link = "0.4" ratatui = "0.29" crossterm = "0.28" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9d445ba..59cc4d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -214,11 +214,13 @@ pub struct Step { pub command: Option, #[serde(default)] pub source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, } impl Step { pub fn is_default(&self) -> bool { - self.active && self.script.is_empty() && self.source.is_none() + self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none() } } @@ -229,6 +231,7 @@ impl Default for Step { script: String::new(), command: None, source: None, + name: None, } } } @@ -252,6 +255,8 @@ struct SparseStep { script: String, #[serde(default, skip_serializing_if = "Option::is_none")] source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, } fn default_active() -> bool { @@ -310,6 +315,7 @@ impl Serialize for Pattern { active: step.active, script: step.script.clone(), source: step.source, + name: step.name.clone(), }) .collect(); @@ -344,6 +350,7 @@ impl<'de> Deserialize<'de> for Pattern { script: ss.script, command: None, source: ss.source, + name: ss.name, }; } } diff --git a/src/app.rs b/src/app.rs index c468c73..45de380 100644 --- a/src/app.rs +++ b/src/app.rs @@ -735,6 +735,7 @@ impl App { active: step.active, source: step.source, original_index: idx, + name: step.name.clone(), }); } } @@ -773,6 +774,7 @@ impl App { let source = if same_pattern { data.source } else { None }; step.active = data.active; step.source = source; + step.name = data.name.clone(); if source.is_some() { step.script.clear(); step.command = None; @@ -1046,6 +1048,17 @@ impl App { } => { self.project_state.project.banks[bank].patterns[pattern].name = name; } + AppCommand::RenameStep { + bank, + pattern, + step, + name, + } => { + if let Some(s) = self.project_state.project.banks[bank].patterns[pattern].step_mut(step) { + s.name = name; + } + self.project_state.mark_dirty(bank, pattern); + } AppCommand::Save(path) => self.save(path, link), AppCommand::Load(path) => self.load(path, link), diff --git a/src/commands.rs b/src/commands.rs index 0197b2e..e514bfc 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -97,6 +97,12 @@ pub enum AppCommand { pattern: usize, name: Option, }, + RenameStep { + bank: usize, + pattern: usize, + step: usize, + name: Option, + }, Save(PathBuf), Load(PathBuf), diff --git a/src/input.rs b/src/input.rs index b5af879..8b4b434 100644 --- a/src/input.rs +++ b/src/input.rs @@ -324,6 +324,34 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { KeyCode::Char(c) => name.push(c), _ => {} }, + Modal::RenameStep { + bank, + pattern, + step, + name, + } => match key.code { + KeyCode::Enter => { + let (bank_idx, pattern_idx, step_idx) = (*bank, *pattern, *step); + let new_name = if name.trim().is_empty() { + None + } else { + Some(name.clone()) + }; + ctx.dispatch(AppCommand::RenameStep { + bank: bank_idx, + pattern: pattern_idx, + step: step_idx, + name: new_name, + }); + ctx.dispatch(AppCommand::CloseModal); + } + KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal), + KeyCode::Backspace => { + name.pop(); + } + KeyCode::Char(c) => name.push(c), + _ => {} + }, Modal::SetPattern { field, input } => match key.code { KeyCode::Enter => { let field = *field; @@ -871,6 +899,23 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR })); } } + 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::RenameStep { + bank, + pattern, + step, + name: current_name, + })); + } KeyCode::Char('?') => { ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); } diff --git a/src/state/editor.rs b/src/state/editor.rs index 125941b..d2b64c2 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -79,6 +79,7 @@ pub struct CopiedStepData { pub active: bool, pub source: Option, pub original_index: usize, + pub name: Option, } impl EditorContext { diff --git a/src/state/modal.rs b/src/state/modal.rs index e6c1f36..1d4d313 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -39,6 +39,12 @@ pub enum Modal { pattern: usize, name: String, }, + RenameStep { + bank: usize, + pattern: usize, + step: usize, + name: String, + }, SetPattern { field: PatternField, input: String, diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 14053be..5207e77 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -184,19 +184,63 @@ fn render_tile( (false, false, false, _, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)), }; + let source_idx = step.and_then(|s| s.source); let symbol = if is_playing { "▶".to_string() - } else if let Some(source) = step.and_then(|s| s.source) { + } else if let Some(source) = source_idx { format!("→{:02}", source + 1) } else { format!("{:02}", step_idx + 1) }; - let tile = Paragraph::new(symbol) - .alignment(Alignment::Center) - .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + // For linked steps, get the name from the source step + let step_name = if let Some(src) = source_idx { + pattern.step(src).and_then(|s| s.name.as_ref()) + } else { + step.and_then(|s| s.name.as_ref()) + }; + let num_lines = if step_name.is_some() { 2u16 } else { 1u16 }; + let content_height = num_lines; + let y_offset = area.height.saturating_sub(content_height) / 2; - frame.render_widget(tile, area); + // Fill background for entire tile + let bg_fill = Paragraph::new("").style(Style::new().bg(bg)); + frame.render_widget(bg_fill, area); + + if let Some(name) = step_name { + let name_area = Rect { + x: area.x, + y: area.y + y_offset, + width: area.width, + height: 1, + }; + let name_widget = Paragraph::new(name.as_str()) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + frame.render_widget(name_widget, name_area); + + let symbol_area = Rect { + x: area.x, + y: area.y + y_offset + 1, + width: area.width, + height: 1, + }; + let symbol_widget = Paragraph::new(symbol) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + frame.render_widget(symbol_widget, symbol_area); + } else { + let centered_area = Rect { + x: area.x, + y: area.y + y_offset, + width: area.width, + height: 1, + }; + let tile = Paragraph::new(symbol) + .alignment(Alignment::Center) + .style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD)); + frame.render_widget(tile, centered_area); + } } fn render_scope(frame: &mut Frame, app: &App, area: Rect) { diff --git a/src/views/render.rs b/src/views/render.rs index f64bc37..1df5ca8 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -577,6 +577,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term .border_color(Color::Magenta) .render_centered(frame, term); } + Modal::RenameStep { step, name, .. } => { + TextInputModal::new(&format!("Name Step {:02}", step + 1), name) + .width(40) + .border_color(Color::Cyan) + .render_centered(frame, term); + } Modal::SetPattern { field, input } => { let (title, hint) = match field { PatternField::Length => ("Set Length (1-128)", "Enter number"), @@ -618,11 +624,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term 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 = if let Some(src) = source_idx { - format!("Step {:02} → {:02}", step_idx + 1, src + 1) - } else { - format!("Step {:02}", step_idx + 1) + 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) @@ -686,6 +694,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let width = (term.width * 80 / 100).max(40); let height = (term.height * 60 / 100).max(10); let step_num = app.editor_ctx.step + 1; + let step = app.current_edit_pattern().step(app.editor_ctx.step); let flash_kind = app.ui.flash_kind(); let border_color = match flash_kind { @@ -695,7 +704,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term None => Color::Rgb(100, 160, 180), }; - let inner = ModalFrame::new(&format!("Step {step_num:02} Script")) + let title = 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)