Feat: ability to rename steps
This commit is contained in:
@@ -18,7 +18,7 @@ path = "src/main.rs"
|
|||||||
cagire-forth = { path = "crates/forth" }
|
cagire-forth = { path = "crates/forth" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
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"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
|
|||||||
@@ -214,11 +214,13 @@ pub struct Step {
|
|||||||
pub command: Option<String>,
|
pub command: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub source: Option<usize>,
|
pub source: Option<usize>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Step {
|
impl Step {
|
||||||
pub fn is_default(&self) -> bool {
|
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(),
|
script: String::new(),
|
||||||
command: None,
|
command: None,
|
||||||
source: None,
|
source: None,
|
||||||
|
name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,6 +255,8 @@ struct SparseStep {
|
|||||||
script: String,
|
script: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
source: Option<usize>,
|
source: Option<usize>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_active() -> bool {
|
fn default_active() -> bool {
|
||||||
@@ -310,6 +315,7 @@ impl Serialize for Pattern {
|
|||||||
active: step.active,
|
active: step.active,
|
||||||
script: step.script.clone(),
|
script: step.script.clone(),
|
||||||
source: step.source,
|
source: step.source,
|
||||||
|
name: step.name.clone(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -344,6 +350,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
script: ss.script,
|
script: ss.script,
|
||||||
command: None,
|
command: None,
|
||||||
source: ss.source,
|
source: ss.source,
|
||||||
|
name: ss.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/app.rs
13
src/app.rs
@@ -735,6 +735,7 @@ impl App {
|
|||||||
active: step.active,
|
active: step.active,
|
||||||
source: step.source,
|
source: step.source,
|
||||||
original_index: idx,
|
original_index: idx,
|
||||||
|
name: step.name.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -773,6 +774,7 @@ impl App {
|
|||||||
let source = if same_pattern { data.source } else { None };
|
let source = if same_pattern { data.source } else { None };
|
||||||
step.active = data.active;
|
step.active = data.active;
|
||||||
step.source = source;
|
step.source = source;
|
||||||
|
step.name = data.name.clone();
|
||||||
if source.is_some() {
|
if source.is_some() {
|
||||||
step.script.clear();
|
step.script.clear();
|
||||||
step.command = None;
|
step.command = None;
|
||||||
@@ -1046,6 +1048,17 @@ impl App {
|
|||||||
} => {
|
} => {
|
||||||
self.project_state.project.banks[bank].patterns[pattern].name = name;
|
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::Save(path) => self.save(path, link),
|
||||||
AppCommand::Load(path) => self.load(path, link),
|
AppCommand::Load(path) => self.load(path, link),
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ pub enum AppCommand {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
|
RenameStep {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
step: usize,
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
Save(PathBuf),
|
Save(PathBuf),
|
||||||
Load(PathBuf),
|
Load(PathBuf),
|
||||||
|
|
||||||
|
|||||||
45
src/input.rs
45
src/input.rs
@@ -324,6 +324,34 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
KeyCode::Char(c) => name.push(c),
|
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 {
|
Modal::SetPattern { field, input } => match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let field = *field;
|
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('?') => {
|
KeyCode::Char('?') => {
|
||||||
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ pub struct CopiedStepData {
|
|||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub source: Option<usize>,
|
pub source: Option<usize>,
|
||||||
pub original_index: usize,
|
pub original_index: usize,
|
||||||
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorContext {
|
impl EditorContext {
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ pub enum Modal {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
RenameStep {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
step: usize,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
SetPattern {
|
SetPattern {
|
||||||
field: PatternField,
|
field: PatternField,
|
||||||
input: String,
|
input: String,
|
||||||
|
|||||||
@@ -184,19 +184,63 @@ fn render_tile(
|
|||||||
(false, false, false, _, _) => (Color::Rgb(45, 48, 55), Color::Rgb(120, 125, 135)),
|
(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 {
|
let symbol = if is_playing {
|
||||||
"▶".to_string()
|
"▶".to_string()
|
||||||
} else if let Some(source) = step.and_then(|s| s.source) {
|
} else if let Some(source) = source_idx {
|
||||||
format!("→{:02}", source + 1)
|
format!("→{:02}", source + 1)
|
||||||
} else {
|
} else {
|
||||||
format!("{:02}", step_idx + 1)
|
format!("{:02}", step_idx + 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
let tile = Paragraph::new(symbol)
|
// For linked steps, get the name from the source step
|
||||||
.alignment(Alignment::Center)
|
let step_name = if let Some(src) = source_idx {
|
||||||
.style(Style::new().bg(bg).fg(fg).add_modifier(Modifier::BOLD));
|
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) {
|
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
|
|||||||
@@ -577,6 +577,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
.border_color(Color::Magenta)
|
.border_color(Color::Magenta)
|
||||||
.render_centered(frame, term);
|
.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 } => {
|
Modal::SetPattern { field, input } => {
|
||||||
let (title, hint) = match field {
|
let (title, hint) = match field {
|
||||||
PatternField::Length => ("Set Length (1-128)", "Enter number"),
|
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_idx = app.editor_ctx.step;
|
||||||
let step = pattern.step(step_idx);
|
let step = pattern.step(step_idx);
|
||||||
let source_idx = step.and_then(|s| s.source);
|
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 {
|
let title = match (source_idx, step_name) {
|
||||||
format!("Step {:02} → {:02}", step_idx + 1, src + 1)
|
(Some(src), Some(name)) => format!("Step {:02}: {} → {:02}", step_idx + 1, name, src + 1),
|
||||||
} else {
|
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
|
||||||
format!("Step {:02}", step_idx + 1)
|
(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)
|
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 width = (term.width * 80 / 100).max(40);
|
||||||
let height = (term.height * 60 / 100).max(10);
|
let height = (term.height * 60 / 100).max(10);
|
||||||
let step_num = app.editor_ctx.step + 1;
|
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 flash_kind = app.ui.flash_kind();
|
||||||
let border_color = match 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),
|
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)
|
.width(width)
|
||||||
.height(height)
|
.height(height)
|
||||||
.border_color(border_color)
|
.border_color(border_color)
|
||||||
|
|||||||
Reference in New Issue
Block a user