Mapper tool for decoding charlieplex

This commit is contained in:
2025-06-27 21:19:59 +02:00
parent 9510ba04be
commit 6eb15fbd12
11 changed files with 1547 additions and 80 deletions

12
mapper/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "mapper"
version = "0.1.0"
edition = "2024"
[dependencies]
jack = "0.11"
kanal = "0.1"
ratatui = "0.26"
crossterm = "0.27"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

26
mapper/format.txt Normal file
View File

@@ -0,0 +1,26 @@
switch 1
switch 2
switches
select
number
value 1
value 2
direct select
midi function
midi chan
config
expression pedal a
expression pedal b
display 1
display 2
display 3
button leds
button presses

169
mapper/mapping.json Normal file
View File

@@ -0,0 +1,169 @@
{
"notes": [
{
"description": "00 00001 0010 00 [G] [G] [G] [2] []",
"register_values": {
"IC03": 253,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00000 1100 00 [P] [P] [P] [3] []",
"register_values": {
"IC03": 254,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00010 0000 01 [] [F] [F] [1] []",
"register_values": {
"IC03": 251,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00100 0000 10 [] [E] [E] [10] []",
"register_values": {
"IC03": 247,
"IC10": 255,
"IC11": 0
}
},
{
"description": "10 01000 0000 00 [] [D] [D] [9] []",
"register_values": {
"IC03": 239,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00000 1100 00 [P] [P] [P] [3] []",
"register_values": {
"IC03": 222,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00001 0010 00 [G] [G] [G] [2] []",
"register_values": {
"IC03": 221,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00010 0000 01 [] [F] [F] [1] []",
"register_values": {
"IC03": 219,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00100 0000 10 [] [E] [E] [10] []",
"register_values": {
"IC03": 215,
"IC10": 255,
"IC11": 0
}
},
{
"description": "10 01000 0000 00 [] [D] [D] [9] []",
"register_values": {
"IC03": 207,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00000 1100 00 [P] [P] [P] [3] []",
"register_values": {
"IC03": 190,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00001 0010 00 [G] [G] [G] [2] []",
"register_values": {
"IC03": 189,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00010 0000 01 [] [F] [F] [1] []",
"register_values": {
"IC03": 187,
"IC10": 255,
"IC11": 0
}
},
{
"description": "00 00100 0000 10 [] [E] [E] [10] []",
"register_values": {
"IC03": 183,
"IC10": 255,
"IC11": 0
}
},
{
"description": "10 01000 0000 00 [] [D] [D] [9] []",
"register_values": {
"IC03": 175,
"IC10": 255,
"IC11": 0
}
},
{
"description": "01 10000 1101 00 [B, C, E, F, G, P] [A, B, C, P] [A, B, C, P] [3, 4, 5, 6, 7, 8] []",
"register_values": {
"IC03": 158,
"IC10": 255,
"IC11": 0
}
},
{
"description": "01 10001 0011 00 [B, C, E, F, G] [A, B, C, G] [A, B, C, G] [2, 4, 5, 6, 7, 8] []",
"register_values": {
"IC03": 157,
"IC10": 255,
"IC11": 0
}
},
{
"description": "01 10001 0010 00 [B, C, E, F, G] [] [A, B, V, G] [2, 4, 5, 6, 7, 8] []",
"register_values": {
"IC03": 157,
"IC10": 253,
"IC11": 0
}
},
{
"description": "00 00000 0000 00 [] [] [] [] [1, 6]",
"register_values": {
"IC03": 255,
"IC10": 255,
"IC11": 4
}
},
{
"description": "00 00000 0000 00 [] [] [] [] [2, 7]",
"register_values": {
"IC03": 255,
"IC10": 255,
"IC11": 8
}
}
],
"register_values": {
"IC03": 255,
"IC10": 255,
"IC11": 8
}
}

959
mapper/src/main.rs Normal file
View File

@@ -0,0 +1,959 @@
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
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,
};
#[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(&current_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(&current_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) = &note.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(&note.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(())
}
fn main() -> Result<(), Box<dyn Error>> {
// Create JACK client
let (client, status) = jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?;
if !status.is_empty() {
eprintln!("JACK client status: {:?}", status);
}
// Create command channel
let (command_sender, command_receiver): (Sender<MidiCommand>, Receiver<MidiCommand>) =
kanal::bounded(32);
// Create MIDI sender
let midi_sender = MidiSender::new(&client, command_receiver)?;
// Activate client
let _active_client = client.activate_async((), midi_sender)?;
// Setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// Create app and run
let app = App::new(command_sender);
let res = run_app(&mut terminal, app);
// Restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err);
}
Ok(())
}