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) }