diff --git a/mapper/mapping.json b/mapper/mapping.json index 2e9cc6b..6e1627a 100644 --- a/mapper/mapping.json +++ b/mapper/mapping.json @@ -25,7 +25,7 @@ } }, { - "description": "00 00100 0000 10 [] [E] [E] [10] []", + "description": "00 00100 0000 10 [] [E] [E] [0] []", "register_values": { "IC03": 247, "IC10": 255, @@ -65,7 +65,7 @@ } }, { - "description": "00 00100 0000 10 [] [E] [E] [10] []", + "description": "00 00100 0000 10 [] [E] [E] [0] []", "register_values": { "IC03": 215, "IC10": 255, @@ -105,7 +105,7 @@ } }, { - "description": "00 00100 0000 10 [] [E] [E] [10] []", + "description": "00 00100 0000 10 [] [E] [E] [0] []", "register_values": { "IC03": 183, "IC10": 255, diff --git a/mapper/src/app.rs b/mapper/src/app.rs new file mode 100644 index 0000000..f35074c --- /dev/null +++ b/mapper/src/app.rs @@ -0,0 +1,306 @@ +use crate::{ + data::{MappingData, StructuredNote}, + input_form::StructuredInputForm, + midi::MidiCommand, +}; +use crossterm::event::KeyCode; +use kanal::Sender; +use ratatui::widgets::ListState; +use serde_json; +use std::{collections::HashMap, error::Error, fs}; + +#[derive(Debug, PartialEq)] +pub enum InputMode { + Normal, + EditValue, + SaveFile, + LoadFile, + StructuredInput, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum SelectedRegister { + IC03, + IC10, + IC11, +} + +impl SelectedRegister { + pub fn as_str(&self) -> &str { + match self { + SelectedRegister::IC03 => "IC03", + SelectedRegister::IC10 => "IC10", + SelectedRegister::IC11 => "IC11", + } + } + + pub fn cc_pair(&self) -> (u8, u8) { + match self { + SelectedRegister::IC03 => (20, 21), + SelectedRegister::IC10 => (22, 23), + SelectedRegister::IC11 => (24, 25), + } + } +} + + +pub struct App { + pub structured_notes: Vec, + pub register_values: HashMap, + pub selected_register: SelectedRegister, + pub input_mode: InputMode, + pub input_buffer: String, + pub cursor_position: usize, + pub notes_list_state: ListState, + pub command_sender: Sender, + pub status_message: String, + pub show_help: bool, + pub structured_form: StructuredInputForm, +} + +impl App { + pub fn new(command_sender: Sender) -> Self { + let register_values = { + let mut map = HashMap::new(); + map.insert("IC03".to_string(), 255); + map.insert("IC10".to_string(), 255); + map.insert("IC11".to_string(), 8); + map + }; + + let structured_form = StructuredInputForm::new(register_values.clone()); + + Self { + structured_notes: Vec::new(), + register_values, + selected_register: SelectedRegister::IC03, + input_mode: InputMode::Normal, + input_buffer: String::new(), + cursor_position: 0, + notes_list_state: ListState::default(), + command_sender, + status_message: "Ready - Press 'n' for new note, ←→ for registers, 'h' for help".to_string(), + show_help: false, + structured_form, + } + } + + pub fn add_structured_note(&mut self) { + let note = self.structured_form.finalize_note(); + + // Check if a note already exists for this register state + let current_values = self.register_values.clone(); + + if let Some(existing_note) = self.structured_notes.iter_mut().find(|n| { + n.register_values == current_values + }) { + *existing_note = note; + self.status_message = "Note updated".to_string(); + } else { + self.structured_notes.push(note); + self.status_message = "Note added".to_string(); + } + } + + pub fn start_structured_input(&mut self) { + self.structured_form.reset(self.register_values.clone()); + + // Check if a note already exists for this register state and pre-load it + let current_values = self.register_values.clone(); + if let Some(existing_note) = self.structured_notes.iter().find(|n| { + n.register_values == current_values + }) { + self.structured_form.current_note = existing_note.clone(); + } + + self.input_mode = InputMode::StructuredInput; + } + + pub fn get_current_value(&self) -> u8 { + *self.register_values.get(self.selected_register.as_str()).unwrap_or(&0) + } + + pub fn set_current_value(&mut self, value: u8) { + self.register_values.insert(self.selected_register.as_str().to_string(), value); + self.send_midi_value(value); + } + + pub fn send_midi_value(&self, value: u8) { + let (cc_low, cc_high) = self.selected_register.cc_pair(); + + if value <= 127 { + let _ = self.command_sender.send(MidiCommand::ControlChange { + cc: cc_low, + value, + }); + } else { + let _ = self.command_sender.send(MidiCommand::ControlChange { + cc: cc_high, + value: value - 128, + }); + } + } + + pub fn toggle_bit(&mut self, bit: u8) { + let current = self.get_current_value(); + let mask = 1 << bit; + let new_value = current ^ mask; + self.set_current_value(new_value); + self.status_message = format!("Toggled {}:bit{} ({})", + self.selected_register.as_str(), bit, new_value); + } + + pub fn save_mapping(&self, filename: &str) -> Result<(), Box> { + // Convert structured notes to legacy format for saving + let legacy_data = MappingData::from_structured_notes( + self.structured_notes.clone(), + self.register_values.clone() + ); + let json = serde_json::to_string_pretty(&legacy_data)?; + fs::write(filename, json)?; + Ok(()) + } + + pub fn load_mapping(&mut self, filename: &str) -> Result<(), Box> { + let content = fs::read_to_string(filename)?; + let legacy_data: MappingData = serde_json::from_str(&content)?; + + // Convert legacy format to structured notes for editing + self.structured_notes = legacy_data.to_structured_notes(); + self.register_values = legacy_data.register_values.clone(); + + // Send current register values to hardware + for (register, &value) in &legacy_data.register_values { + match register.as_str() { + "IC03" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC03; + self.send_midi_value(value); + self.selected_register = old_selected; + } + "IC10" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC10; + self.send_midi_value(value); + self.selected_register = old_selected; + } + "IC11" => { + let old_selected = self.selected_register; + self.selected_register = SelectedRegister::IC11; + self.send_midi_value(value); + self.selected_register = old_selected; + } + _ => {} + } + } + + Ok(()) + } + + pub fn handle_structured_input(&mut self, key: KeyCode) -> Result> { + match key { + KeyCode::Enter => { + if !self.structured_form.current_note.is_empty() { + self.add_structured_note(); + } + self.input_mode = InputMode::Normal; + } + KeyCode::Esc => { + self.input_mode = InputMode::Normal; + } + KeyCode::Char(' ') => { + let next_field = self.structured_form.current_field.next(); + let text_len = self.structured_form.get_text_value(&next_field).len(); + self.structured_form.current_field = next_field; + self.structured_form.cursor_position = text_len; + } + KeyCode::Up => { + let prev_field = self.structured_form.current_field.prev(); + let text_len = self.structured_form.get_text_value(&prev_field).len(); + self.structured_form.current_field = prev_field; + self.structured_form.cursor_position = text_len; + } + KeyCode::Down => { + let next_field = self.structured_form.current_field.next(); + let text_len = self.structured_form.get_text_value(&next_field).len(); + self.structured_form.current_field = next_field; + self.structured_form.cursor_position = text_len; + } + _ => { + if self.structured_form.current_field.is_led_field() { + self.handle_led_input(key); + } else if self.structured_form.current_field.is_text_field() { + self.handle_text_input(key); + } + } + } + Ok(false) + } + + fn handle_led_input(&mut self, key: KeyCode) { + if let KeyCode::Char(c) = key { + if c == '0' || c == '1' { + let value = if c == '1' { Some(1) } else { Some(0) }; + let current_field = self.structured_form.current_field; + self.structured_form.set_led_value(¤t_field, value); + // Auto-advance to next field + self.structured_form.current_field = self.structured_form.current_field.next(); + } + } + } + + fn handle_text_input(&mut self, key: KeyCode) { + let current_field = self.structured_form.current_field; + let cursor_pos = self.structured_form.cursor_position; + + if let Some(current_text) = self.structured_form.get_text_value_mut(¤t_field) { + match key { + KeyCode::Char(c) => { + current_text.insert(cursor_pos, c); + } + KeyCode::Backspace => { + if cursor_pos > 0 { + current_text.remove(cursor_pos - 1); + } + } + KeyCode::Delete => { + if cursor_pos < current_text.len() { + current_text.remove(cursor_pos); + } + } + _ => {} + } + } + + // Update cursor position after text operations + match key { + KeyCode::Char(_) => { + self.structured_form.cursor_position += 1; + } + KeyCode::Backspace => { + if cursor_pos > 0 { + self.structured_form.cursor_position -= 1; + } + } + KeyCode::Left => { + if cursor_pos > 0 { + self.structured_form.cursor_position -= 1; + } + } + KeyCode::Right => { + let text_len = self.structured_form.get_text_value(¤t_field).len(); + if cursor_pos < text_len { + self.structured_form.cursor_position += 1; + } + } + KeyCode::Home => { + self.structured_form.cursor_position = 0; + } + KeyCode::End => { + let text_len = self.structured_form.get_text_value(¤t_field).len(); + self.structured_form.cursor_position = text_len; + } + _ => {} + } + } +} \ No newline at end of file diff --git a/mapper/src/data.rs b/mapper/src/data.rs new file mode 100644 index 0000000..dd2ba80 --- /dev/null +++ b/mapper/src/data.rs @@ -0,0 +1,261 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StructuredNote { + pub description: String, + pub register_values: HashMap, + + // LED states (0/1) + pub switch_1: Option, + pub switch_2: Option, + pub switches: Option, + pub select: Option, + pub number: Option, + pub value_1: Option, + pub value_2: Option, + pub direct_select: Option, + pub midi_function: Option, + pub midi_chan: Option, + pub config: Option, + pub expression_pedal_a: Option, + pub expression_pedal_b: Option, + + // Display segments (strings of active segments) + pub display_1: String, + pub display_2: String, + pub display_3: String, + + // Button states (strings of active button numbers) + pub button_leds: String, + pub button_presses: String, +} + +impl Default for StructuredNote { + fn default() -> Self { + Self { + description: String::new(), + register_values: HashMap::new(), + switch_1: None, + switch_2: None, + switches: None, + select: None, + number: None, + value_1: None, + value_2: None, + direct_select: None, + midi_function: None, + midi_chan: None, + config: None, + expression_pedal_a: None, + expression_pedal_b: None, + display_1: String::new(), + display_2: String::new(), + display_3: String::new(), + button_leds: String::new(), + button_presses: String::new(), + } + } +} + +// Legacy format for file I/O compatibility +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterNote { + pub description: String, + pub register_values: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MappingData { + pub notes: Vec, + pub register_values: HashMap, +} + +impl Default for MappingData { + fn default() -> Self { + let mut register_values = HashMap::new(); + register_values.insert("IC03".to_string(), 255); + register_values.insert("IC10".to_string(), 255); + register_values.insert("IC11".to_string(), 8); + + Self { + notes: Vec::new(), + register_values, + } + } +} + +impl StructuredNote { + pub fn is_empty(&self) -> bool { + self.switch_1.is_none() && + self.switch_2.is_none() && + self.display_1.is_empty() && + self.display_2.is_empty() && + self.display_3.is_empty() && + self.button_leds.is_empty() && + self.button_presses.is_empty() + } + + pub fn generate_description(&self) -> String { + let format_list = |text: &str| -> String { + if text.is_empty() { + String::new() + } else { + text.chars().map(|c| c.to_string()).collect::>().join(", ") + } + }; + + format!("{}{} {}{}{}{}{} {}{}{}{} {}{} [{}] [{}] [{}] [{}] [{}]", + self.switch_1.unwrap_or(0), + self.switch_2.unwrap_or(0), + self.switches.unwrap_or(0), + self.select.unwrap_or(0), + self.number.unwrap_or(0), + self.value_1.unwrap_or(0), + self.value_2.unwrap_or(0), + self.direct_select.unwrap_or(0), + self.midi_function.unwrap_or(0), + self.midi_chan.unwrap_or(0), + self.config.unwrap_or(0), + self.expression_pedal_a.unwrap_or(0), + self.expression_pedal_b.unwrap_or(0), + format_list(&self.display_1), + format_list(&self.display_2), + format_list(&self.display_3), + format_list(&self.button_leds), + format_list(&self.button_presses) + ) + } + + pub fn to_register_note(&self) -> RegisterNote { + RegisterNote { + description: self.generate_description(), + register_values: Some(self.register_values.clone()), + } + } + + pub fn from_register_note(note: &RegisterNote) -> Option { + // Parse the description to extract structured data + // Format: "01 10000 1101 00 [B, C, E, F, G, P] [A, B, C, P] [A, B, C, P] [3, 4, 5, 6, 7, 8] []" + let description = ¬e.description; + let mut structured_note = StructuredNote { + description: description.clone(), + register_values: note.register_values.clone().unwrap_or_default(), + ..Default::default() + }; + + // Split the description into parts + let parts: Vec<&str> = description.split_whitespace().collect(); + + if parts.len() >= 4 { + // Parse LED states from first 4 parts + // Part 0: switch1+switch2 (e.g., "01") + if let Some(switches) = parts.get(0) { + if switches.len() >= 2 { + if let Some(c) = switches.chars().nth(0) { + structured_note.switch_1 = c.to_digit(10).map(|d| d as u8); + } + if let Some(c) = switches.chars().nth(1) { + structured_note.switch_2 = c.to_digit(10).map(|d| d as u8); + } + } + } + + // Part 1: switches+select+number+value1+value2 (e.g., "10000") + if let Some(middle_bits) = parts.get(1) { + let chars: Vec = middle_bits.chars().collect(); + if chars.len() >= 5 { + structured_note.switches = chars[0].to_digit(10).map(|d| d as u8); + structured_note.select = chars[1].to_digit(10).map(|d| d as u8); + structured_note.number = chars[2].to_digit(10).map(|d| d as u8); + structured_note.value_1 = chars[3].to_digit(10).map(|d| d as u8); + structured_note.value_2 = chars[4].to_digit(10).map(|d| d as u8); + } + } + + // Part 2: direct_select+midi_function+midi_chan+config (e.g., "1101") + if let Some(last_bits) = parts.get(2) { + let chars: Vec = last_bits.chars().collect(); + if chars.len() >= 4 { + structured_note.direct_select = chars[0].to_digit(10).map(|d| d as u8); + structured_note.midi_function = chars[1].to_digit(10).map(|d| d as u8); + structured_note.midi_chan = chars[2].to_digit(10).map(|d| d as u8); + structured_note.config = chars[3].to_digit(10).map(|d| d as u8); + } + } + + // Part 3: expression_pedal_a+expression_pedal_b (e.g., "00") + if let Some(pedals) = parts.get(3) { + let chars: Vec = pedals.chars().collect(); + if chars.len() >= 2 { + structured_note.expression_pedal_a = chars[0].to_digit(10).map(|d| d as u8); + structured_note.expression_pedal_b = chars[1].to_digit(10).map(|d| d as u8); + } + } + } + + // Parse the bracketed sections for text fields + let bracket_sections = Self::extract_bracket_sections(description); + if bracket_sections.len() >= 5 { + structured_note.display_1 = bracket_sections[0].clone(); + structured_note.display_2 = bracket_sections[1].clone(); + structured_note.display_3 = bracket_sections[2].clone(); + structured_note.button_leds = bracket_sections[3].clone(); + structured_note.button_presses = bracket_sections[4].clone(); + } + + Some(structured_note) + } + + fn extract_bracket_sections(text: &str) -> Vec { + let mut sections = Vec::new(); + let mut current_section = String::new(); + let mut inside_brackets = false; + + for ch in text.chars() { + match ch { + '[' => { + inside_brackets = true; + current_section.clear(); + } + ']' => { + if inside_brackets { + // Convert comma-separated to no commas for storage + let clean_section = current_section + .split(", ") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect::>() + .join(""); + sections.push(clean_section); + inside_brackets = false; + } + } + _ => { + if inside_brackets { + current_section.push(ch); + } + } + } + } + + sections + } +} + +impl MappingData { + pub fn from_structured_notes(notes: Vec, register_values: HashMap) -> Self { + let legacy_notes = notes.iter().map(|n| n.to_register_note()).collect(); + Self { + notes: legacy_notes, + register_values, + } + } + + pub fn to_structured_notes(&self) -> Vec { + self.notes + .iter() + .filter_map(StructuredNote::from_register_note) + .collect() + } +} \ No newline at end of file diff --git a/mapper/src/input_form.rs b/mapper/src/input_form.rs new file mode 100644 index 0000000..6001209 --- /dev/null +++ b/mapper/src/input_form.rs @@ -0,0 +1,284 @@ +use crate::data::StructuredNote; +use ratatui::{ + layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum InputField { + Switch1, + Switch2, + Switches, + Select, + Number, + Value1, + Value2, + DirectSelect, + MidiFunction, + MidiChan, + Config, + ExpressionPedalA, + ExpressionPedalB, + Display1, + Display2, + Display3, + ButtonLeds, + ButtonPresses, +} + +impl InputField { + pub fn next(&self) -> Self { + match self { + InputField::Switch1 => InputField::Switch2, + InputField::Switch2 => InputField::Switches, + InputField::Switches => InputField::Select, + InputField::Select => InputField::Number, + InputField::Number => InputField::Value1, + InputField::Value1 => InputField::Value2, + InputField::Value2 => InputField::DirectSelect, + InputField::DirectSelect => InputField::MidiFunction, + InputField::MidiFunction => InputField::MidiChan, + InputField::MidiChan => InputField::Config, + InputField::Config => InputField::ExpressionPedalA, + InputField::ExpressionPedalA => InputField::ExpressionPedalB, + InputField::ExpressionPedalB => InputField::Display1, + InputField::Display1 => InputField::Display2, + InputField::Display2 => InputField::Display3, + InputField::Display3 => InputField::ButtonLeds, + InputField::ButtonLeds => InputField::ButtonPresses, + InputField::ButtonPresses => InputField::Switch1, // Wrap around + } + } + + pub fn prev(&self) -> Self { + match self { + InputField::Switch1 => InputField::ButtonPresses, + InputField::Switch2 => InputField::Switch1, + InputField::Switches => InputField::Switch2, + InputField::Select => InputField::Switches, + InputField::Number => InputField::Select, + InputField::Value1 => InputField::Number, + InputField::Value2 => InputField::Value1, + InputField::DirectSelect => InputField::Value2, + InputField::MidiFunction => InputField::DirectSelect, + InputField::MidiChan => InputField::MidiFunction, + InputField::Config => InputField::MidiChan, + InputField::ExpressionPedalA => InputField::Config, + InputField::ExpressionPedalB => InputField::ExpressionPedalA, + InputField::Display1 => InputField::ExpressionPedalB, + InputField::Display2 => InputField::Display1, + InputField::Display3 => InputField::Display2, + InputField::ButtonLeds => InputField::Display3, + InputField::ButtonPresses => InputField::ButtonLeds, + } + } + + pub fn name(&self) -> &str { + match self { + InputField::Switch1 => "Switch 1", + InputField::Switch2 => "Switch 2", + InputField::Switches => "Switches", + InputField::Select => "Select", + InputField::Number => "Number", + InputField::Value1 => "Value 1", + InputField::Value2 => "Value 2", + InputField::DirectSelect => "Direct Select", + InputField::MidiFunction => "MIDI Function", + InputField::MidiChan => "MIDI Chan", + InputField::Config => "Config", + InputField::ExpressionPedalA => "Expression Pedal A", + InputField::ExpressionPedalB => "Expression Pedal B", + InputField::Display1 => "Display 1", + InputField::Display2 => "Display 2", + InputField::Display3 => "Display 3", + InputField::ButtonLeds => "Button LEDs", + InputField::ButtonPresses => "Button Presses", + } + } + + pub fn is_led_field(&self) -> bool { + matches!(self, + InputField::Switch1 | InputField::Switch2 | InputField::Switches | + InputField::Select | InputField::Number | InputField::Value1 | + InputField::Value2 | InputField::DirectSelect | InputField::MidiFunction | + InputField::MidiChan | InputField::Config | InputField::ExpressionPedalA | + InputField::ExpressionPedalB + ) + } + + pub fn is_text_field(&self) -> bool { + matches!(self, + InputField::Display1 | InputField::Display2 | InputField::Display3 | + InputField::ButtonLeds | InputField::ButtonPresses + ) + } +} + +pub struct StructuredInputForm { + pub current_note: StructuredNote, + pub current_field: InputField, + pub cursor_position: usize, +} + +impl StructuredInputForm { + pub fn new(register_values: HashMap) -> Self { + let mut current_note = StructuredNote::default(); + current_note.register_values = register_values; + + Self { + current_note, + current_field: InputField::Switch1, + cursor_position: 0, + } + } + + pub fn reset(&mut self, register_values: HashMap) { + self.current_note = StructuredNote::default(); + self.current_note.register_values = register_values; + self.current_field = InputField::Switch1; + self.cursor_position = 0; + } + + pub fn get_led_value(&self, field: &InputField) -> Option { + match field { + InputField::Switch1 => self.current_note.switch_1, + InputField::Switch2 => self.current_note.switch_2, + InputField::Switches => self.current_note.switches, + InputField::Select => self.current_note.select, + InputField::Number => self.current_note.number, + InputField::Value1 => self.current_note.value_1, + InputField::Value2 => self.current_note.value_2, + InputField::DirectSelect => self.current_note.direct_select, + InputField::MidiFunction => self.current_note.midi_function, + InputField::MidiChan => self.current_note.midi_chan, + InputField::Config => self.current_note.config, + InputField::ExpressionPedalA => self.current_note.expression_pedal_a, + InputField::ExpressionPedalB => self.current_note.expression_pedal_b, + _ => None, + } + } + + pub fn get_text_value(&self, field: &InputField) -> &str { + match field { + InputField::Display1 => &self.current_note.display_1, + InputField::Display2 => &self.current_note.display_2, + InputField::Display3 => &self.current_note.display_3, + InputField::ButtonLeds => &self.current_note.button_leds, + InputField::ButtonPresses => &self.current_note.button_presses, + _ => "", + } + } + + pub fn set_led_value(&mut self, field: &InputField, value: Option) { + match field { + InputField::Switch1 => self.current_note.switch_1 = value, + InputField::Switch2 => self.current_note.switch_2 = value, + InputField::Switches => self.current_note.switches = value, + InputField::Select => self.current_note.select = value, + InputField::Number => self.current_note.number = value, + InputField::Value1 => self.current_note.value_1 = value, + InputField::Value2 => self.current_note.value_2 = value, + InputField::DirectSelect => self.current_note.direct_select = value, + InputField::MidiFunction => self.current_note.midi_function = value, + InputField::MidiChan => self.current_note.midi_chan = value, + InputField::Config => self.current_note.config = value, + InputField::ExpressionPedalA => self.current_note.expression_pedal_a = value, + InputField::ExpressionPedalB => self.current_note.expression_pedal_b = value, + _ => {} + } + } + + pub fn get_text_value_mut(&mut self, field: &InputField) -> Option<&mut String> { + match field { + InputField::Display1 => Some(&mut self.current_note.display_1), + InputField::Display2 => Some(&mut self.current_note.display_2), + InputField::Display3 => Some(&mut self.current_note.display_3), + InputField::ButtonLeds => Some(&mut self.current_note.button_leds), + InputField::ButtonPresses => Some(&mut self.current_note.button_presses), + _ => None, + } + } + + pub fn finalize_note(&mut self) -> StructuredNote { + self.current_note.description = self.current_note.generate_description(); + self.current_note.clone() + } +} + +pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, area: Rect) { + let mut lines = Vec::new(); + + // Helper function to create field line + let create_field_line = |field: &InputField, form: &StructuredInputForm| -> Line { + let is_current = form.current_field == *field; + let prefix = if is_current { "→ " } else { " " }; + let style = if is_current { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + + if field.is_led_field() { + let value = form.get_led_value(field); + let value_str = match value { Some(1) => "1", Some(0) => "0", _ => "─" }; + Line::from(vec![Span::styled(format!("{}{}: {}", prefix, field.name().to_lowercase(), value_str), style)]) + } else { + let text = form.get_text_value(field); + let display_text = if is_current { + let mut display = text.to_string(); + if form.cursor_position <= display.len() { + display.insert(form.cursor_position, '│'); + } + display + } else if text.is_empty() { + "".to_string() + } else { + text.to_string() + }; + Line::from(vec![Span::styled(format!("{}{} (list of active segments): {}", prefix, field.name().to_lowercase(), display_text), style)]) + } + }; + + // Switch controls + lines.push(create_field_line(&InputField::Switch1, form)); + lines.push(create_field_line(&InputField::Switch2, form)); + lines.push(Line::from("")); + + // Main LED controls + lines.push(create_field_line(&InputField::Switches, form)); + lines.push(create_field_line(&InputField::Select, form)); + lines.push(create_field_line(&InputField::Number, form)); + lines.push(create_field_line(&InputField::Value1, form)); + lines.push(create_field_line(&InputField::Value2, form)); + lines.push(Line::from("")); + + // Additional controls + lines.push(create_field_line(&InputField::DirectSelect, form)); + lines.push(create_field_line(&InputField::MidiFunction, form)); + lines.push(create_field_line(&InputField::MidiChan, form)); + lines.push(create_field_line(&InputField::Config, form)); + lines.push(Line::from("")); + + // Expression pedals + lines.push(create_field_line(&InputField::ExpressionPedalA, form)); + lines.push(create_field_line(&InputField::ExpressionPedalB, form)); + lines.push(Line::from("")); + + // Display segments + lines.push(create_field_line(&InputField::Display1, form)); + lines.push(create_field_line(&InputField::Display2, form)); + lines.push(create_field_line(&InputField::Display3, form)); + lines.push(Line::from("")); + + // Button controls + lines.push(create_field_line(&InputField::ButtonLeds, form)); + lines.push(create_field_line(&InputField::ButtonPresses, form)); + + let block = Paragraph::new(lines) + .block(Block::default().borders(Borders::ALL).title("Structured Note Input")); + f.render_widget(block, area); +} \ No newline at end of file diff --git a/mapper/src/main.rs b/mapper/src/main.rs index 49eb20f..909ba84 100644 --- a/mapper/src/main.rs +++ b/mapper/src/main.rs @@ -1,918 +1,23 @@ use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + event::{DisableMouseCapture, EnableMouseCapture}, }; -use jack::RawMidi; -use kanal::{Receiver, Sender}; use ratatui::{ - backend::{Backend, CrosstermBackend}, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{ - Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap, - }, - Frame, Terminal, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - error::Error, - fs, - io, + backend::CrosstermBackend, + Terminal, }; +use kanal::{Receiver, Sender}; +use std::{error::Error, io}; -#[derive(Debug)] -enum MidiCommand { - ControlChange { cc: u8, value: u8 }, - Quit, -} - -struct MidiSender { - midi_out: jack::Port, - command_receiver: Receiver, -} - -impl MidiSender { - fn new( - client: &jack::Client, - command_receiver: Receiver, - ) -> Result> { - let midi_out = client - .register_port("fcb1010_mapper", jack::MidiOut::default()) - .map_err(|e| format!("Could not create MIDI output port: {}", e))?; - - Ok(Self { - midi_out, - command_receiver, - }) - } -} - -impl jack::ProcessHandler for MidiSender { - fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { - let mut midi_writer = self.midi_out.writer(ps); - - while let Ok(Some(command)) = self.command_receiver.try_recv() { - match command { - MidiCommand::ControlChange { cc, value } => { - let midi_data = [0xB0, cc, value]; // Control Change on channel 1 - if let Err(e) = midi_writer.write(&RawMidi { - time: 0, - bytes: &midi_data, - }) { - eprintln!("Failed to send MIDI: {}", e); - } - } - MidiCommand::Quit => return jack::Control::Quit, - } - } - - jack::Control::Continue - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct RegisterNote { - description: String, - register_values: Option>, // Store all register values -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct MappingData { - notes: Vec, - register_values: HashMap, -} - -impl Default for MappingData { - fn default() -> Self { - let mut register_values = HashMap::new(); - register_values.insert("IC03".to_string(), 0); - register_values.insert("IC10".to_string(), 0); - register_values.insert("IC11".to_string(), 0); - - Self { - notes: Vec::new(), - register_values, - } - } -} - -#[derive(Debug, PartialEq)] -enum InputMode { - Normal, - EditValue, - AddNote, - SaveFile, - LoadFile, -} - -#[derive(Debug, PartialEq, Clone, Copy)] -enum SelectedRegister { - IC03, - IC10, - IC11, -} - -impl SelectedRegister { - fn as_str(&self) -> &str { - match self { - SelectedRegister::IC03 => "IC03", - SelectedRegister::IC10 => "IC10", - SelectedRegister::IC11 => "IC11", - } - } - - fn cc_pair(&self) -> (u8, u8) { - match self { - SelectedRegister::IC03 => (20, 21), - SelectedRegister::IC10 => (22, 23), - SelectedRegister::IC11 => (24, 25), - } - } -} - -struct App { - mapping_data: MappingData, - selected_register: SelectedRegister, - input_mode: InputMode, - input_buffer: String, - cursor_position: usize, - notes_list_state: ListState, - command_sender: Sender, - status_message: String, - show_help: bool, -} - -impl App { - fn new(command_sender: Sender) -> Self { - Self { - mapping_data: MappingData::default(), - selected_register: SelectedRegister::IC03, - input_mode: InputMode::Normal, - input_buffer: String::new(), - cursor_position: 0, - notes_list_state: ListState::default(), - command_sender, - status_message: "Ready - Press 'h' for help".to_string(), - show_help: false, - } - } - - fn get_current_value(&self) -> u8 { - *self.mapping_data.register_values.get(self.selected_register.as_str()).unwrap_or(&0) - } - - fn set_current_value(&mut self, value: u8) { - self.mapping_data.register_values.insert(self.selected_register.as_str().to_string(), value); - self.send_midi_value(value); - } - - fn send_midi_value(&self, value: u8) { - let (cc_low, cc_high) = self.selected_register.cc_pair(); - - if value <= 127 { - let _ = self.command_sender.send(MidiCommand::ControlChange { - cc: cc_low, - value, - }); - } else { - let _ = self.command_sender.send(MidiCommand::ControlChange { - cc: cc_high, - value: value - 128, - }); - } - } - - fn toggle_bit(&mut self, bit: u8) { - let current = self.get_current_value(); - let mask = 1 << bit; - let new_value = current ^ mask; - self.set_current_value(new_value); - self.status_message = format!("Toggled {}:bit{} ({})", - self.selected_register.as_str(), bit, new_value); - } - - fn add_note(&mut self, description: String) { - // Check if a note already exists for this register state - let current_values = self.mapping_data.register_values.clone(); - - // Find existing note with same register values - if let Some(existing_note) = self.mapping_data.notes.iter_mut().find(|note| { - note.register_values.as_ref() == Some(¤t_values) - }) { - // Update existing note - existing_note.description = description; - existing_note.register_values = Some(current_values); - self.status_message = "Note updated".to_string(); - } else { - // Create new note - let note = RegisterNote { - description, - register_values: Some(current_values), - }; - self.mapping_data.notes.push(note); - self.status_message = "Note added".to_string(); - } - } - - fn save_mapping(&self, filename: &str) -> Result<(), Box> { - let json = serde_json::to_string_pretty(&self.mapping_data)?; - fs::write(filename, json)?; - Ok(()) - } - - fn load_mapping(&mut self, filename: &str) -> Result<(), Box> { - let content = fs::read_to_string(filename)?; - self.mapping_data = serde_json::from_str(&content)?; - - // Send current register values to hardware - for (register, &value) in &self.mapping_data.register_values { - match register.as_str() { - "IC03" => { - self.selected_register = SelectedRegister::IC03; - self.send_midi_value(value); - } - "IC10" => { - self.selected_register = SelectedRegister::IC10; - self.send_midi_value(value); - } - "IC11" => { - self.selected_register = SelectedRegister::IC11; - self.send_midi_value(value); - } - _ => {} - } - } - - Ok(()) - } - - fn handle_input(&mut self, key: KeyCode) -> Result> { - match self.input_mode { - InputMode::Normal => self.handle_normal_input(key), - InputMode::EditValue => self.handle_edit_value_input(key), - InputMode::AddNote => self.handle_add_note_input(key), - InputMode::SaveFile => self.handle_save_file_input(key), - InputMode::LoadFile => self.handle_load_file_input(key), - } - } - - fn handle_normal_input(&mut self, key: KeyCode) -> Result> { - match key { - KeyCode::Char('q') => { - let _ = self.command_sender.send(MidiCommand::Quit); - return Ok(true); - } - KeyCode::Char('h') => self.show_help = !self.show_help, - KeyCode::Left => { - self.selected_register = match self.selected_register { - SelectedRegister::IC03 => SelectedRegister::IC11, - SelectedRegister::IC10 => SelectedRegister::IC03, - SelectedRegister::IC11 => SelectedRegister::IC10, - }; - } - KeyCode::Right => { - self.selected_register = match self.selected_register { - SelectedRegister::IC03 => SelectedRegister::IC10, - SelectedRegister::IC10 => SelectedRegister::IC11, - SelectedRegister::IC11 => SelectedRegister::IC03, - }; - } - KeyCode::Char(c @ '0'..='7') => { - if let Some(bit) = c.to_digit(10) { - self.toggle_bit(bit as u8); - } - } - KeyCode::Char('r') => { - self.set_current_value(0); - self.status_message = format!("{} reset to 0", self.selected_register.as_str()); - } - KeyCode::Char('f') => { - self.set_current_value(255); - self.status_message = format!("{} set to 255", self.selected_register.as_str()); - } - KeyCode::Char('v') => { - self.input_mode = InputMode::EditValue; - self.input_buffer = self.get_current_value().to_string(); - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Char('n') => { - // Check if a note already exists for this register state - let current_values = self.mapping_data.register_values.clone(); - - // Find existing note with same register values and pre-load its text - if let Some(existing_note) = self.mapping_data.notes.iter().find(|note| { - note.register_values.as_ref() == Some(¤t_values) - }) { - self.input_buffer = existing_note.description.clone(); - } else { - self.input_buffer.clear(); - } - - self.cursor_position = self.input_buffer.chars().count(); - self.input_mode = InputMode::AddNote; - } - KeyCode::Char('s') => { - self.input_mode = InputMode::SaveFile; - self.input_buffer = "mapping.json".to_string(); - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Char('l') => { - self.input_mode = InputMode::LoadFile; - self.input_buffer = "mapping.json".to_string(); - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Up => { - if let Some(selected) = self.notes_list_state.selected() { - if selected > 0 { - self.notes_list_state.select(Some(selected - 1)); - } - } else if !self.mapping_data.notes.is_empty() { - self.notes_list_state.select(Some(0)); - } - } - KeyCode::Down => { - if let Some(selected) = self.notes_list_state.selected() { - if selected < self.mapping_data.notes.len().saturating_sub(1) { - self.notes_list_state.select(Some(selected + 1)); - } - } else if !self.mapping_data.notes.is_empty() { - self.notes_list_state.select(Some(0)); - } - } - KeyCode::Enter => { - if let Some(selected) = self.notes_list_state.selected() { - if let Some(note) = self.mapping_data.notes.get(selected) { - // Restore register values from note - if let Some(values) = note.register_values.as_ref() { - for (register, &value) in values { - self.mapping_data.register_values.insert(register.clone(), value); - // Send MIDI for each register - match register.as_str() { - "IC03" => { - let old_selected = self.selected_register; - self.selected_register = SelectedRegister::IC03; - self.send_midi_value(value); - self.selected_register = old_selected; - } - "IC10" => { - let old_selected = self.selected_register; - self.selected_register = SelectedRegister::IC10; - self.send_midi_value(value); - self.selected_register = old_selected; - } - "IC11" => { - let old_selected = self.selected_register; - self.selected_register = SelectedRegister::IC11; - self.send_midi_value(value); - self.selected_register = old_selected; - } - _ => {} - } - } - self.status_message = "State restored from note".to_string(); - } - } - } - } - KeyCode::Delete => { - if let Some(selected) = self.notes_list_state.selected() { - if selected < self.mapping_data.notes.len() { - self.mapping_data.notes.remove(selected); - if self.mapping_data.notes.is_empty() { - self.notes_list_state.select(None); - } else if selected >= self.mapping_data.notes.len() { - self.notes_list_state.select(Some(self.mapping_data.notes.len() - 1)); - } - self.status_message = "Note deleted".to_string(); - } - } - } - _ => {} - } - Ok(false) - } - - fn handle_edit_value_input(&mut self, key: KeyCode) -> Result> { - match key { - KeyCode::Enter => { - match self.input_buffer.parse::() { - Ok(value) => { - self.set_current_value(value); - self.status_message = format!("{} set to {}", - self.selected_register.as_str(), value); - } - Err(_) => { - self.status_message = "Invalid value (0-255)".to_string(); - } - } - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Esc => { - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Left => { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - KeyCode::Right => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - self.cursor_position += 1; - } - } - KeyCode::Home => { - self.cursor_position = 0; - } - KeyCode::End => { - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Backspace => { - if self.cursor_position > 0 { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position - 1); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position -= 1; - } - } - KeyCode::Delete => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position); - self.input_buffer = chars.into_iter().collect(); - } - } - KeyCode::Char(c) => { - if let Ok(potential_value) = format!("{}{}", &self.input_buffer[..self.cursor_position], c).parse::() { - if potential_value <= 255 { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.insert(self.cursor_position, c); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position += 1; - } - } else if self.input_buffer.len() < 3 { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.insert(self.cursor_position, c); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position += 1; - } - } - _ => {} - } - Ok(false) - } - - fn handle_add_note_input(&mut self, key: KeyCode) -> Result> { - match key { - KeyCode::Enter => { - if !self.input_buffer.trim().is_empty() { - self.add_note(self.input_buffer.clone()); - } - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Esc => { - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Left => { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - KeyCode::Right => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - self.cursor_position += 1; - } - } - KeyCode::Home => { - self.cursor_position = 0; - } - KeyCode::End => { - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Backspace => { - if self.cursor_position > 0 { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position - 1); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position -= 1; - } - } - KeyCode::Delete => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position); - self.input_buffer = chars.into_iter().collect(); - } - } - KeyCode::Char(c) => { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.insert(self.cursor_position, c); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position += 1; - } - _ => {} - } - Ok(false) - } - - fn handle_save_file_input(&mut self, key: KeyCode) -> Result> { - match key { - KeyCode::Enter => { - match self.save_mapping(&self.input_buffer) { - Ok(_) => self.status_message = format!("Saved to {}", self.input_buffer), - Err(e) => self.status_message = format!("Save failed: {}", e), - } - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Esc => { - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Left => { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - KeyCode::Right => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - self.cursor_position += 1; - } - } - KeyCode::Home => { - self.cursor_position = 0; - } - KeyCode::End => { - self.cursor_position = self.input_buffer.chars().count(); - } - KeyCode::Backspace => { - if self.cursor_position > 0 { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position - 1); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position -= 1; - } - } - KeyCode::Delete => { - let char_count = self.input_buffer.chars().count(); - if self.cursor_position < char_count { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.remove(self.cursor_position); - self.input_buffer = chars.into_iter().collect(); - } - } - KeyCode::Char(c) => { - let mut chars: Vec = self.input_buffer.chars().collect(); - chars.insert(self.cursor_position, c); - self.input_buffer = chars.into_iter().collect(); - self.cursor_position += 1; - } - _ => {} - } - Ok(false) - } - - fn handle_load_file_input(&mut self, key: KeyCode) -> Result> { - match key { - KeyCode::Enter => { - let filename = self.input_buffer.clone(); - match self.load_mapping(&filename) { - Ok(_) => self.status_message = format!("Loaded from {}", filename), - Err(e) => self.status_message = format!("Load failed: {}", e), - } - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Esc => { - self.input_mode = InputMode::Normal; - self.input_buffer.clear(); - self.cursor_position = 0; - } - KeyCode::Left => { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - KeyCode::Right => { - if self.cursor_position < self.input_buffer.len() { - self.cursor_position += 1; - } - } - KeyCode::Home => { - self.cursor_position = 0; - } - KeyCode::End => { - self.cursor_position = self.input_buffer.len(); - } - KeyCode::Backspace => { - if self.cursor_position > 0 { - self.input_buffer.remove(self.cursor_position - 1); - self.cursor_position -= 1; - } - } - KeyCode::Delete => { - if self.cursor_position < self.input_buffer.len() { - self.input_buffer.remove(self.cursor_position); - } - } - KeyCode::Char(c) => { - self.input_buffer.insert(self.cursor_position, c); - self.cursor_position += 1; - } - _ => {} - } - Ok(false) - } -} - -fn ui(f: &mut Frame, app: &mut App) { - if app.show_help { - draw_help(f); - return; - } - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Length(7), - Constraint::Min(5), - Constraint::Length(3), - ]) - .split(f.size()); - - // Title - let title = Paragraph::new("FCB1010 Register Mapper") - .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) - .alignment(Alignment::Center) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(title, chunks[0]); - - // Register values - draw_register_values(f, app, chunks[1]); - - // Notes list - draw_notes_list(f, app, chunks[2]); - - // Status and input - draw_status_bar(f, app, chunks[3]); - - // Input popup if needed - if app.input_mode != InputMode::Normal { - draw_input_popup(f, app); - } -} - -fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) - .split(area); - - let registers = [ - (&SelectedRegister::IC03, "IC03"), - (&SelectedRegister::IC10, "IC10"), - (&SelectedRegister::IC11, "IC11"), - ]; - - for (i, (register, name)) in registers.iter().enumerate() { - let value = *app.mapping_data.register_values.get(*name).unwrap_or(&0); - let is_selected = app.selected_register == **register; - - let mut lines = Vec::new(); - lines.push(Line::from(vec![ - Span::styled(format!("{}: ", name), Style::default().add_modifier(Modifier::BOLD)), - Span::raw(format!("{:08b} = {}", value, value)), - ])); - - // Bit representation - let mut bit_spans = Vec::new(); - for bit in (0..8).rev() { - let is_set = (value >> bit) & 1 == 1; - let style = if is_set { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::DarkGray) - }; - bit_spans.push(Span::styled(format!("[{}]", if is_set { "■" } else { "□" }), style)); - } - lines.push(Line::from(bit_spans)); - - // Bit numbers - let mut bit_num_spans = Vec::new(); - for bit in (0..8).rev() { - bit_num_spans.push(Span::styled(format!(" {} ", bit), Style::default().fg(Color::Yellow))); - } - lines.push(Line::from(bit_num_spans)); - - let border_style = if is_selected { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }; - - let block = Block::default() - .borders(Borders::ALL) - .style(border_style) - .title(if is_selected { format!("→ {}", name) } else { name.to_string() }); - - let paragraph = Paragraph::new(lines).block(block); - f.render_widget(paragraph, chunks[i]); - } -} - -fn draw_notes_list(f: &mut Frame, app: &mut App, area: Rect) { - let items: Vec = app - .mapping_data - .notes - .iter() - .map(|note| { - let mut content = String::new(); - - // Add register values in hex format first - if let Some(values) = ¬e.register_values { - let ic03 = values.get("IC03").unwrap_or(&0); - let ic10 = values.get("IC10").unwrap_or(&0); - let ic11 = values.get("IC11").unwrap_or(&0); - content.push_str(&format!("[IC03:0x{:02X}, IC10:0x{:02X}, IC11:0x{:02X}]", ic03, ic10, ic11)); - } - - // Add dash separator and note description - content.push_str(" - "); - content.push_str(¬e.description); - - ListItem::new(content) - }) - .collect(); - - let notes_list = List::new(items) - .block(Block::default().borders(Borders::ALL).title("Notes (↑↓ to navigate, Enter to restore, Del to remove)")) - .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - .highlight_symbol("→ "); - - f.render_stateful_widget(notes_list, area, &mut app.notes_list_state); -} - -fn draw_status_bar(f: &mut Frame, app: &App, area: Rect) { - let status_text = match app.input_mode { - InputMode::Normal => format!("Status: {} | Commands: ←→:select reg, 0-7:toggle bit, v:edit value, n:add note, s:save, l:load, r:reset, f:fill, h:help, q:quit", app.status_message), - InputMode::EditValue => format!("Enter value (0-255): {}", app.input_buffer), - InputMode::AddNote => format!("Enter note: {}", app.input_buffer), - InputMode::SaveFile => format!("Save filename: {}", app.input_buffer), - InputMode::LoadFile => format!("Load filename: {}", app.input_buffer), - }; - - let paragraph = Paragraph::new(status_text) - .style(Style::default().fg(Color::White)) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(paragraph, area); -} - -fn draw_input_popup(f: &mut Frame, app: &App) { - let area = centered_rect(60, 20, f.size()); - f.render_widget(Clear, area); - - let title = match app.input_mode { - InputMode::EditValue => "Edit Value", - InputMode::AddNote => "Add/Edit Note", - InputMode::SaveFile => "Save File", - InputMode::LoadFile => "Load File", - _ => "Input", - }; - - let block = Block::default() - .title(title) - .borders(Borders::ALL) - .style(Style::default().fg(Color::Yellow)); - - // Create text with cursor using char-aware operations - let mut spans = Vec::new(); - let chars: Vec = app.input_buffer.chars().collect(); - - if chars.is_empty() { - spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); - } else { - if app.cursor_position > 0 { - let before_cursor: String = chars.iter().take(app.cursor_position).collect(); - spans.push(Span::raw(before_cursor)); - } - - if app.cursor_position < chars.len() { - let cursor_char = chars[app.cursor_position]; - spans.push(Span::styled(cursor_char.to_string(), Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); - - if app.cursor_position + 1 < chars.len() { - let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect(); - spans.push(Span::raw(after_cursor)); - } - } else { - spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); - } - } - - let paragraph = Paragraph::new(Line::from(spans)) - .block(block); - - f.render_widget(paragraph, area); -} - -fn draw_help(f: &mut Frame) { - let help_text = vec![ - "FCB1010 Register Mapper - Help", - "", - "Register Selection:", - " ← → - Select IC03, IC10, IC11", - "", - "Bit Control:", - " 0-7 - Toggle bit 0-7 of selected register", - " v - Edit register value directly (0-255)", - " r - Reset register to 0", - " f - Fill register (set to 255)", - "", - "Notes:", - " n - Add note for current register/bit", - " ↑↓ - Navigate notes list", - " Enter - Restore register values from selected note", - " Del - Delete selected note", - "", - "File Operations:", - " s - Save mapping to file", - " l - Load mapping from file", - "", - "Other:", - " h - Toggle this help", - " q - Quit", - "", - "Press 'h' again to return to main view", - ]; - - let help_paragraph = Paragraph::new( - help_text - .iter() - .map(|&line| Line::from(line)) - .collect::>() - ) - .block(Block::default().title("Help").borders(Borders::ALL)) - .wrap(Wrap { trim: true }); - - f.render_widget(help_paragraph, f.size()); -} - -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] -} - -fn run_app( - terminal: &mut Terminal, - mut app: App, -) -> Result<(), Box> { - loop { - terminal.draw(|f| ui(f, &mut app))?; - - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - if app.handle_input(key.code)? { - break; - } - } - } - } - Ok(()) -} +mod midi; +mod data; +mod input_form; +mod ui; +mod app; +use app::App; +use midi::{MidiCommand, MidiSender}; +use ui::run_app; fn main() -> Result<(), Box> { // Create JACK client diff --git a/mapper/src/midi.rs b/mapper/src/midi.rs new file mode 100644 index 0000000..43c67a0 --- /dev/null +++ b/mapper/src/midi.rs @@ -0,0 +1,53 @@ +use jack::RawMidi; +use kanal::Receiver; +use std::error::Error; + +#[derive(Debug)] +pub enum MidiCommand { + ControlChange { cc: u8, value: u8 }, + Quit, +} + +pub struct MidiSender { + midi_out: jack::Port, + command_receiver: Receiver, +} + +impl MidiSender { + pub fn new( + client: &jack::Client, + command_receiver: Receiver, + ) -> Result> { + let midi_out = client + .register_port("fcb1010_mapper", jack::MidiOut::default()) + .map_err(|e| format!("Could not create MIDI output port: {}", e))?; + + Ok(Self { + midi_out, + command_receiver, + }) + } +} + +impl jack::ProcessHandler for MidiSender { + fn process(&mut self, _client: &jack::Client, ps: &jack::ProcessScope) -> jack::Control { + let mut midi_writer = self.midi_out.writer(ps); + + while let Ok(Some(command)) = self.command_receiver.try_recv() { + match command { + MidiCommand::ControlChange { cc, value } => { + let midi_data = [0xB0, cc, value]; // Control Change on channel 1 + if let Err(e) = midi_writer.write(&RawMidi { + time: 0, + bytes: &midi_data, + }) { + eprintln!("Failed to send MIDI: {}", e); + } + } + MidiCommand::Quit => return jack::Control::Quit, + } + } + + jack::Control::Continue + } +} \ No newline at end of file diff --git a/mapper/src/ui.rs b/mapper/src/ui.rs new file mode 100644 index 0000000..764bc94 --- /dev/null +++ b/mapper/src/ui.rs @@ -0,0 +1,651 @@ +use crate::{ + app::{App, InputMode, SelectedRegister}, + input_form::draw_structured_input_form, +}; +use ratatui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{ + Block, Borders, Clear, List, ListItem, Paragraph, Wrap, + }, + Frame, Terminal, +}; +use std::error::Error; + +pub fn ui(f: &mut Frame, app: &mut App) { + if app.show_help { + draw_help(f); + return; + } + + draw_structured_ui(f, app); + + // Input popup if needed + if matches!(app.input_mode, InputMode::EditValue | InputMode::SaveFile | InputMode::LoadFile) { + draw_input_popup(f, app); + } +} + +fn draw_structured_ui(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(7), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(f.size()); + + // Title + let title = Paragraph::new("FCB1010 Structured Note Mapper") + .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, chunks[0]); + + // Register values + draw_register_values(f, app, chunks[1]); + + // Main content area + if app.input_mode == InputMode::StructuredInput { + draw_structured_input_form(f, &app.structured_form, chunks[2]); + } else { + draw_structured_notes_list(f, app, chunks[2]); + } + + // Status bar + let status_text = if app.input_mode == InputMode::StructuredInput { + "Enter: Save note | Esc: Cancel | Space/↑↓: Navigate fields | 0/1: LED state | Letters: Text input" + } else { + "←→: Select register | 0-7: Toggle bit | v: Edit value | r: Reset | f: Fill | n: New note | ↑↓: Navigate | Del: Delete | s: Save | l: Load | h: Help | q: Quit" + }; + + let status = Paragraph::new(status_text) + .style(Style::default().fg(Color::White)) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(status, chunks[3]); +} + +fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) { + let items: Vec = app + .structured_notes + .iter() + .enumerate() + .map(|(i, note)| { + let mut content = String::new(); + + // Add register values in binary format first + let ic03 = note.register_values.get("IC03").unwrap_or(&0); + let ic10 = note.register_values.get("IC10").unwrap_or(&0); + let ic11 = note.register_values.get("IC11").unwrap_or(&0); + content.push_str(&format!("[IC03:{:08b}, IC10:{:08b}, IC11:{:08b}]", ic03, ic10, ic11)); + + // Add dash separator and note description + content.push_str(&format!(" - {}. {}", i + 1, note.description)); + + ListItem::new(content) + }) + .collect(); + + let mut notes_list_state = app.notes_list_state.clone(); + let notes_list = List::new(items) + .block(Block::default().borders(Borders::ALL).title("Structured Notes (Enter to restore register values, Del to remove)")) + .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .highlight_symbol("→ "); + + f.render_stateful_widget(notes_list, area, &mut notes_list_state); +} + +fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) + .split(area); + + let registers = [ + (&SelectedRegister::IC03, "IC03"), + (&SelectedRegister::IC10, "IC10"), + (&SelectedRegister::IC11, "IC11"), + ]; + + for (i, (register, name)) in registers.iter().enumerate() { + let value = *app.register_values.get(*name).unwrap_or(&0); + let is_selected = app.selected_register == **register; + + let mut lines = Vec::new(); + lines.push(Line::from(vec![ + Span::styled(format!("{}: ", name), Style::default().add_modifier(Modifier::BOLD)), + Span::raw(format!("{:08b} = {}", value, value)), + ])); + + // Bit representation + let mut bit_spans = Vec::new(); + for bit in (0..8).rev() { + let is_set = (value >> bit) & 1 == 1; + let style = if is_set { + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + bit_spans.push(Span::styled(format!("[{}]", if is_set { "■" } else { "□" }), style)); + } + lines.push(Line::from(bit_spans)); + + // Bit numbers + let mut bit_num_spans = Vec::new(); + for bit in (0..8).rev() { + bit_num_spans.push(Span::styled(format!(" {} ", bit), Style::default().fg(Color::Yellow))); + } + lines.push(Line::from(bit_num_spans)); + + let border_style = if is_selected { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }; + + let block = Block::default() + .borders(Borders::ALL) + .style(border_style) + .title(if is_selected { format!("→ {}", name) } else { name.to_string() }); + + let paragraph = Paragraph::new(lines).block(block); + f.render_widget(paragraph, chunks[i]); + } +} + + +fn draw_input_popup(f: &mut Frame, app: &App) { + let area = centered_rect(60, 20, f.size()); + f.render_widget(Clear, area); + + let title = match app.input_mode { + InputMode::EditValue => "Edit Value", + InputMode::SaveFile => "Save File", + InputMode::LoadFile => "Load File", + _ => "Input", + }; + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .style(Style::default().fg(Color::Yellow)); + + // Create text with cursor using char-aware operations + let mut spans = Vec::new(); + let chars: Vec = app.input_buffer.chars().collect(); + + if chars.is_empty() { + spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + } else { + if app.cursor_position > 0 { + let before_cursor: String = chars.iter().take(app.cursor_position).collect(); + spans.push(Span::raw(before_cursor)); + } + + if app.cursor_position < chars.len() { + let cursor_char = chars[app.cursor_position]; + spans.push(Span::styled(cursor_char.to_string(), Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + + if app.cursor_position + 1 < chars.len() { + let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect(); + spans.push(Span::raw(after_cursor)); + } + } else { + spans.push(Span::styled("█", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); + } + } + + let paragraph = Paragraph::new(Line::from(spans)) + .block(block); + + f.render_widget(paragraph, area); +} + +fn draw_help(f: &mut Frame) { + let help_text = vec![ + "FCB1010 Structured Note Mapper - Help", + "", + "Register Control:", + " ←→ - Select IC03, IC10, IC11 register", + " 0-7 - Toggle bit 0-7 of selected register", + " v - Edit register value directly (0-255)", + " r - Reset register to 0", + " f - Fill register (set to 255)", + "", + "Notes Management:", + " n - Create new structured note", + " ↑↓ - Navigate notes list", + " Enter - Restore register values from selected note", + " Del - Delete selected note", + "", + "File Operations:", + " s - Save mapping to file", + " l - Load mapping from file", + "", + "In Structured Note Input:", + " 0/1 - Set LED state for current field", + " Space/↑↓ - Navigate between fields", + " Text input - For display segments and button fields", + " Enter - Save structured note", + " Esc - Cancel input", + "", + "LED Fields:", + " Switch 1, Switch 2, Switches, Select, Number,", + " Value 1, Value 2, Direct Select, MIDI Function,", + " MIDI Chan, Config, Expression Pedal A/B", + "", + "Text Fields:", + " Display 1/2/3 - Active display segments", + " Button LEDs - Active button LEDs", + " Button Presses - Buttons that react to press", + "", + "File Format:", + " Saves/loads in original format for compatibility", + " Register editor + structured notes for full control", + "", + "Other:", + " h - Toggle this help", + " q - Quit", + "", + "Press 'h' again to return to main view", + ]; + + + let help_paragraph = Paragraph::new( + help_text + .iter() + .map(|&line| Line::from(line)) + .collect::>() + ) + .block(Block::default().title("Help").borders(Borders::ALL)) + .wrap(Wrap { trim: true }); + + f.render_widget(help_paragraph, f.size()); +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +} + +pub fn run_app( + terminal: &mut Terminal, + mut app: App, +) -> Result<(), Box> { + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let crossterm::event::Event::Key(key) = crossterm::event::read()? { + if key.kind == crossterm::event::KeyEventKind::Press { + if handle_input(&mut app, key.code)? { + break; + } + } + } + } + Ok(()) +} + +pub fn handle_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + match app.input_mode { + InputMode::StructuredInput => app.handle_structured_input(key), + _ => handle_legacy_input(app, key), + } +} + +fn handle_legacy_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + match app.input_mode { + InputMode::Normal => handle_normal_input(app, key), + InputMode::EditValue => handle_edit_value_input(app, key), + InputMode::SaveFile => handle_save_file_input(app, key), + InputMode::LoadFile => handle_load_file_input(app, key), + _ => Ok(false), + } +} + +fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + use crossterm::event::KeyCode; + + match key { + KeyCode::Char('q') => { + let _ = app.command_sender.send(crate::midi::MidiCommand::Quit); + return Ok(true); + } + KeyCode::Char('h') => app.show_help = !app.show_help, + KeyCode::Char('n') => { + app.start_structured_input(); + } + KeyCode::Left => { + app.selected_register = match app.selected_register { + SelectedRegister::IC03 => SelectedRegister::IC11, + SelectedRegister::IC10 => SelectedRegister::IC03, + SelectedRegister::IC11 => SelectedRegister::IC10, + }; + } + KeyCode::Right => { + app.selected_register = match app.selected_register { + SelectedRegister::IC03 => SelectedRegister::IC10, + SelectedRegister::IC10 => SelectedRegister::IC11, + SelectedRegister::IC11 => SelectedRegister::IC03, + }; + } + KeyCode::Char(c @ '0'..='7') => { + if let Some(bit) = c.to_digit(10) { + app.toggle_bit(bit as u8); + } + } + KeyCode::Char('r') => { + app.set_current_value(0); + app.status_message = format!("{} reset to 0", app.selected_register.as_str()); + } + KeyCode::Char('f') => { + app.set_current_value(255); + app.status_message = format!("{} set to 255", app.selected_register.as_str()); + } + KeyCode::Char('v') => { + app.input_mode = InputMode::EditValue; + app.input_buffer = app.get_current_value().to_string(); + app.cursor_position = app.input_buffer.chars().count(); + } + KeyCode::Char('s') => { + app.input_mode = InputMode::SaveFile; + app.input_buffer = "mapping.json".to_string(); + app.cursor_position = app.input_buffer.chars().count(); + } + KeyCode::Char('l') => { + app.input_mode = InputMode::LoadFile; + app.input_buffer = "mapping.json".to_string(); + app.cursor_position = app.input_buffer.chars().count(); + } + KeyCode::Up => { + let notes_len = app.structured_notes.len(); + + if let Some(selected) = app.notes_list_state.selected() { + if selected > 0 { + app.notes_list_state.select(Some(selected - 1)); + } + } else if notes_len > 0 { + app.notes_list_state.select(Some(0)); + } + } + KeyCode::Down => { + let notes_len = app.structured_notes.len(); + + if let Some(selected) = app.notes_list_state.selected() { + if selected < notes_len.saturating_sub(1) { + app.notes_list_state.select(Some(selected + 1)); + } + } else if notes_len > 0 { + app.notes_list_state.select(Some(0)); + } + } + KeyCode::Enter => { + if let Some(selected) = app.notes_list_state.selected() { + if let Some(note) = app.structured_notes.get(selected) { + // Restore register values from note + app.register_values = note.register_values.clone(); + app.status_message = "Register values restored from note".to_string(); + + // Send MIDI updates for all registers + for (register_name, &value) in &app.register_values { + match register_name.as_str() { + "IC03" => { + let old_selected = app.selected_register; + app.selected_register = SelectedRegister::IC03; + app.send_midi_value(value); + app.selected_register = old_selected; + } + "IC10" => { + let old_selected = app.selected_register; + app.selected_register = SelectedRegister::IC10; + app.send_midi_value(value); + app.selected_register = old_selected; + } + "IC11" => { + let old_selected = app.selected_register; + app.selected_register = SelectedRegister::IC11; + app.send_midi_value(value); + app.selected_register = old_selected; + } + _ => {} + } + } + } + } + } + KeyCode::Delete => { + if let Some(selected) = app.notes_list_state.selected() { + if selected < app.structured_notes.len() { + app.structured_notes.remove(selected); + if app.structured_notes.is_empty() { + app.notes_list_state.select(None); + } else if selected >= app.structured_notes.len() { + app.notes_list_state.select(Some(app.structured_notes.len() - 1)); + } + app.status_message = "Note deleted".to_string(); + } + } + } + _ => {} + } + Ok(false) +} + +// Implement other input handlers (edit_value, save_file, load_file) + +fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + use crossterm::event::KeyCode; + + match key { + KeyCode::Enter => { + match app.input_buffer.parse::() { + Ok(value) => { + app.set_current_value(value); + app.status_message = format!("{} set to {}", + app.selected_register.as_str(), value); + } + Err(_) => { + app.status_message = "Invalid value (0-255)".to_string(); + } + } + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Esc => { + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Left => { + if app.cursor_position > 0 { + app.cursor_position -= 1; + } + } + KeyCode::Right => { + let char_count = app.input_buffer.chars().count(); + if app.cursor_position < char_count { + app.cursor_position += 1; + } + } + KeyCode::Home => { + app.cursor_position = 0; + } + KeyCode::End => { + app.cursor_position = app.input_buffer.chars().count(); + } + KeyCode::Backspace => { + if app.cursor_position > 0 { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.remove(app.cursor_position - 1); + app.input_buffer = chars.into_iter().collect(); + app.cursor_position -= 1; + } + } + KeyCode::Delete => { + let char_count = app.input_buffer.chars().count(); + if app.cursor_position < char_count { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.remove(app.cursor_position); + app.input_buffer = chars.into_iter().collect(); + } + } + KeyCode::Char(c) => { + if let Ok(potential_value) = format!("{}{}", &app.input_buffer[..app.cursor_position], c).parse::() { + if potential_value <= 255 { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.insert(app.cursor_position, c); + app.input_buffer = chars.into_iter().collect(); + app.cursor_position += 1; + } + } else if app.input_buffer.len() < 3 { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.insert(app.cursor_position, c); + app.input_buffer = chars.into_iter().collect(); + app.cursor_position += 1; + } + } + _ => {} + } + Ok(false) +} + +fn handle_save_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + use crossterm::event::KeyCode; + + match key { + KeyCode::Enter => { + match app.save_mapping(&app.input_buffer) { + Ok(_) => app.status_message = format!("Saved to {}", app.input_buffer), + Err(e) => app.status_message = format!("Save failed: {}", e), + } + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Esc => { + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Left => { + if app.cursor_position > 0 { + app.cursor_position -= 1; + } + } + KeyCode::Right => { + let char_count = app.input_buffer.chars().count(); + if app.cursor_position < char_count { + app.cursor_position += 1; + } + } + KeyCode::Home => { + app.cursor_position = 0; + } + KeyCode::End => { + app.cursor_position = app.input_buffer.chars().count(); + } + KeyCode::Backspace => { + if app.cursor_position > 0 { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.remove(app.cursor_position - 1); + app.input_buffer = chars.into_iter().collect(); + app.cursor_position -= 1; + } + } + KeyCode::Delete => { + let char_count = app.input_buffer.chars().count(); + if app.cursor_position < char_count { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.remove(app.cursor_position); + app.input_buffer = chars.into_iter().collect(); + } + } + KeyCode::Char(c) => { + let mut chars: Vec = app.input_buffer.chars().collect(); + chars.insert(app.cursor_position, c); + app.input_buffer = chars.into_iter().collect(); + app.cursor_position += 1; + } + _ => {} + } + Ok(false) +} + +fn handle_load_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result> { + use crossterm::event::KeyCode; + + match key { + KeyCode::Enter => { + let filename = app.input_buffer.clone(); + match app.load_mapping(&filename) { + Ok(_) => { + if app.status_message.is_empty() { + app.status_message = format!("Loaded from {}", filename); + } + } + Err(e) => app.status_message = format!("Load failed: {}", e), + } + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Esc => { + app.input_mode = InputMode::Normal; + app.input_buffer.clear(); + app.cursor_position = 0; + } + KeyCode::Left => { + if app.cursor_position > 0 { + app.cursor_position -= 1; + } + } + KeyCode::Right => { + if app.cursor_position < app.input_buffer.len() { + app.cursor_position += 1; + } + } + KeyCode::Home => { + app.cursor_position = 0; + } + KeyCode::End => { + app.cursor_position = app.input_buffer.len(); + } + KeyCode::Backspace => { + if app.cursor_position > 0 { + app.input_buffer.remove(app.cursor_position - 1); + app.cursor_position -= 1; + } + } + KeyCode::Delete => { + if app.cursor_position < app.input_buffer.len() { + app.input_buffer.remove(app.cursor_position); + } + } + KeyCode::Char(c) => { + app.input_buffer.insert(app.cursor_position, c); + app.cursor_position += 1; + } + _ => {} + } + Ok(false) +} \ No newline at end of file