Better notes editor
This commit is contained in:
parent
6eb15fbd12
commit
d1b88ff921
@ -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,
|
||||
|
||||
306
mapper/src/app.rs
Normal file
306
mapper/src/app.rs
Normal file
@ -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<StructuredNote>,
|
||||
pub register_values: HashMap<String, u8>,
|
||||
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<MidiCommand>,
|
||||
pub status_message: String,
|
||||
pub show_help: bool,
|
||||
pub structured_form: StructuredInputForm,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(command_sender: Sender<MidiCommand>) -> 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<dyn Error>> {
|
||||
// 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<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
261
mapper/src/data.rs
Normal file
261
mapper/src/data.rs
Normal file
@ -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<String, u8>,
|
||||
|
||||
// LED states (0/1)
|
||||
pub switch_1: Option<u8>,
|
||||
pub switch_2: Option<u8>,
|
||||
pub switches: Option<u8>,
|
||||
pub select: Option<u8>,
|
||||
pub number: Option<u8>,
|
||||
pub value_1: Option<u8>,
|
||||
pub value_2: Option<u8>,
|
||||
pub direct_select: Option<u8>,
|
||||
pub midi_function: Option<u8>,
|
||||
pub midi_chan: Option<u8>,
|
||||
pub config: Option<u8>,
|
||||
pub expression_pedal_a: Option<u8>,
|
||||
pub expression_pedal_b: Option<u8>,
|
||||
|
||||
// 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<HashMap<String, u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MappingData {
|
||||
pub notes: Vec<RegisterNote>,
|
||||
pub register_values: HashMap<String, u8>,
|
||||
}
|
||||
|
||||
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::<Vec<_>>().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<Self> {
|
||||
// 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<char> = 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<char> = 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<char> = 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<String> {
|
||||
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::<Vec<_>>()
|
||||
.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<StructuredNote>, register_values: HashMap<String, u8>) -> 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<StructuredNote> {
|
||||
self.notes
|
||||
.iter()
|
||||
.filter_map(StructuredNote::from_register_note)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
284
mapper/src/input_form.rs
Normal file
284
mapper/src/input_form.rs
Normal file
@ -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<String, u8>) -> 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<String, u8>) {
|
||||
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<u8> {
|
||||
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<u8>) {
|
||||
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);
|
||||
}
|
||||
@ -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<jack::MidiOut>,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
}
|
||||
|
||||
impl MidiSender {
|
||||
fn new(
|
||||
client: &jack::Client,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
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<HashMap<String, u8>>, // Store all register values
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct MappingData {
|
||||
notes: Vec<RegisterNote>,
|
||||
register_values: HashMap<String, u8>,
|
||||
}
|
||||
|
||||
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<MidiCommand>,
|
||||
status_message: String,
|
||||
show_help: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new(command_sender: Sender<MidiCommand>) -> 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<dyn Error>> {
|
||||
let json = serde_json::to_string_pretty(&self.mapping_data)?;
|
||||
fs::write(filename, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_mapping(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
match key {
|
||||
KeyCode::Enter => {
|
||||
match self.input_buffer.parse::<u8>() {
|
||||
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<char> = 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<char> = 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::<u16>() {
|
||||
if potential_value <= 255 {
|
||||
let mut chars: Vec<char> = 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<char> = 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<bool, Box<dyn Error>> {
|
||||
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<char> = 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<char> = self.input_buffer.chars().collect();
|
||||
chars.remove(self.cursor_position);
|
||||
self.input_buffer = chars.into_iter().collect();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut chars: Vec<char> = 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<bool, Box<dyn Error>> {
|
||||
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<char> = 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<char> = self.input_buffer.chars().collect();
|
||||
chars.remove(self.cursor_position);
|
||||
self.input_buffer = chars.into_iter().collect();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut chars: Vec<char> = 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<bool, Box<dyn Error>> {
|
||||
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<ListItem> = 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<char> = 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::<Vec<_>>()
|
||||
)
|
||||
.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<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
// Create JACK client
|
||||
|
||||
53
mapper/src/midi.rs
Normal file
53
mapper/src/midi.rs
Normal file
@ -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<jack::MidiOut>,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
}
|
||||
|
||||
impl MidiSender {
|
||||
pub fn new(
|
||||
client: &jack::Client,
|
||||
command_receiver: Receiver<MidiCommand>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
651
mapper/src/ui.rs
Normal file
651
mapper/src/ui.rs
Normal file
@ -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<ListItem> = 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<char> = 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::<Vec<_>>()
|
||||
)
|
||||
.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<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
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<bool, Box<dyn Error>> {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
match key {
|
||||
KeyCode::Enter => {
|
||||
match app.input_buffer.parse::<u8>() {
|
||||
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<char> = 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<char> = 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::<u16>() {
|
||||
if potential_value <= 255 {
|
||||
let mut chars: Vec<char> = 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<char> = 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<bool, Box<dyn Error>> {
|
||||
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<char> = 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<char> = app.input_buffer.chars().collect();
|
||||
chars.remove(app.cursor_position);
|
||||
app.input_buffer = chars.into_iter().collect();
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
let mut chars: Vec<char> = 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<bool, Box<dyn Error>> {
|
||||
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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user