This commit is contained in:
geens 2025-08-12 16:10:02 +02:00
commit 66d0c1218f
20 changed files with 486 additions and 183 deletions

View File

@ -90,6 +90,14 @@ impl<const ROWS: usize> Column<ROWS> {
self.tracks[row].set_consolidated_buffer(buffer) self.tracks[row].set_consolidated_buffer(buffer)
} }
pub fn is_all_tracks_cleared(&self) -> bool {
self.tracks.iter().all(|track| track.is_empty())
}
pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) {
self.frames_per_beat = new_frames_per_beat;
}
pub fn handle_xrun( pub fn handle_xrun(
&mut self, &mut self,
timing: &BufferTiming, timing: &BufferTiming,

View File

@ -16,6 +16,7 @@ mod persistence_manager;
mod post_record_handler; mod post_record_handler;
mod process_handler; mod process_handler;
mod state; mod state;
mod tap_tempo;
mod track; mod track;
mod track_matrix; mod track_matrix;
@ -47,6 +48,7 @@ use post_record_handler::PostRecordHandler;
use post_record_handler::PostRecordResponse; use post_record_handler::PostRecordResponse;
use process_handler::ProcessHandler; use process_handler::ProcessHandler;
use state::State; use state::State;
use tap_tempo::TapTempo;
use track::Track; use track::Track;
use track::TrackState; use track::TrackState;
use track_matrix::TrackMatrix; use track_matrix::TrackMatrix;

View File

@ -68,6 +68,22 @@ impl Metronome {
Ok(timing) Ok(timing)
} }
pub fn set_click_volume(
&mut self,
click_volume: f32,
persistence: &mut PersistenceManagerController,
) -> Result<()> {
let clamped = click_volume.clamp(0.0, 1.0);
persistence.update_metronome_volume(clamped)?;
self.click_volume = clamped;
Ok(())
}
pub fn set_frames_per_beat(&mut self, frames_per_beat: usize) {
self.frames_per_beat = frames_per_beat;
self.frames_since_last_beat = self.frames_since_last_beat % self.frames_per_beat;
}
fn process_ppqn_ticks( fn process_ppqn_ticks(
&mut self, &mut self,
ps: &jack::ProcessScope, ps: &jack::ProcessScope,

View File

@ -38,11 +38,14 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
let value_num = u8::from(value); let value_num = u8::from(value);
match controller_num { match controller_num {
// Expression Pedal A - Track Volume // Expression Pedals
1 => { 1 => {
process_handler.handle_pedal_a(value_num)?; process_handler.handle_pedal_a(value_num)?;
} }
// Button mappings (only process button presses - value > 0) 2 => {
process_handler.handle_pedal_b(value_num)?;
}
// Buttons
20 if value_num > 0 => { 20 if value_num > 0 => {
process_handler.handle_button_1()?; process_handler.handle_button_1()?;
} }
@ -56,7 +59,7 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
process_handler.handle_button_4()?; process_handler.handle_button_4()?;
} }
24 if value_num > 0 => { 24 if value_num > 0 => {
process_handler.handle_button_5()?; process_handler.handle_button_5(ps)?;
} }
25 if value_num > 0 => { 25 if value_num > 0 => {
process_handler.handle_button_6()?; process_handler.handle_button_6()?;
@ -79,19 +82,16 @@ pub fn process_events<F: ChunkFactory, const COLS: usize, const ROWS: usize>(
31 if value_num > 0 => { 31 if value_num > 0 => {
process_handler.handle_button_down()?; process_handler.handle_button_down()?;
} }
_ => {
// Other CC messages - ignore // Other CC messages - ignore
_ => {}
} }
} }
}
_ => {
// Other MIDI messages - ignore // Other MIDI messages - ignore
_ => {}
} }
} }
}
Err(_) => {
// Malformed MIDI messages - ignore // Malformed MIDI messages - ignore
} Err(_) => {}
} }
} }

View File

@ -14,6 +14,12 @@ pub enum PersistenceUpdate {
row: usize, row: usize,
volume: f32, volume: f32,
}, },
UpdateMetronomeVolume {
volume: f32,
},
UpdateMetronomeFramesPerBeat {
frames_per_beat: usize,
},
} }
impl PersistenceManagerController { impl PersistenceManagerController {
@ -29,6 +35,24 @@ impl PersistenceManagerController {
Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed
} }
} }
pub fn update_metronome_volume(&self, volume: f32) -> Result<()> {
let update = PersistenceUpdate::UpdateMetronomeVolume { volume };
match self.sender.try_send(update) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed
}
}
pub fn update_metronome_frames_per_beat(&self, frames_per_beat: usize) -> Result<()> {
let update = PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat };
match self.sender.try_send(update) {
Ok(true) => Ok(()),
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
Err(_) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel closed
}
}
} }
pub struct PersistenceManager { pub struct PersistenceManager {
@ -105,6 +129,16 @@ impl PersistenceManager {
.send_modify(|state| state.track_volumes.set_volume(column, row, volume)); .send_modify(|state| state.track_volumes.set_volume(column, row, volume));
self.save_to_disk()?; self.save_to_disk()?;
} }
PersistenceUpdate::UpdateMetronomeVolume { volume } => {
self.state
.send_modify(|state| state.metronome.click_volume = volume);
self.save_to_disk()?;
}
PersistenceUpdate::UpdateMetronomeFramesPerBeat { frames_per_beat } => {
self.state
.send_modify(|state| state.metronome.frames_per_beat = frames_per_beat);
self.save_to_disk()?;
}
} }
Ok(()) Ok(())

View File

@ -11,6 +11,7 @@ pub struct ProcessHandler<F: ChunkFactory, const COLS: usize, const ROWS: usize>
selected_column: usize, selected_column: usize,
last_volume_setting: f32, last_volume_setting: f32,
sample_rate: usize, sample_rate: usize,
tap_tempo: TapTempo,
} }
impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> { impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, COLS, ROWS> {
@ -36,6 +37,7 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
selected_column: 0, selected_column: 0,
last_volume_setting: 1.0, last_volume_setting: 1.0,
sample_rate: client.sample_rate(), sample_rate: client.sample_rate(),
tap_tempo: TapTempo::new(client.sample_rate()),
}) })
} }
@ -58,6 +60,11 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
.handle_volume_update(self.last_volume_setting, &controllers) .handle_volume_update(self.last_volume_setting, &controllers)
} }
pub fn handle_pedal_b(&mut self, value: u8) -> Result<()> {
self.metronome
.set_click_volume(value as f32 / 127.0, &mut self.persistence)
}
pub fn handle_button_1(&mut self) -> Result<()> { pub fn handle_button_1(&mut self) -> Result<()> {
let controllers = TrackControllers::new( let controllers = TrackControllers::new(
&self.post_record_controller, &self.post_record_controller,
@ -88,10 +95,6 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
} }
pub fn handle_button_4(&mut self) -> Result<()> { pub fn handle_button_4(&mut self) -> Result<()> {
Ok(())
}
pub fn handle_button_5(&mut self) -> Result<()> {
let controllers = TrackControllers::new( let controllers = TrackControllers::new(
&self.post_record_controller, &self.post_record_controller,
&self.osc, &self.osc,
@ -103,6 +106,27 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
self.track_matrix.handle_clear_button(&controllers) self.track_matrix.handle_clear_button(&controllers)
} }
pub fn handle_button_5(&mut self, ps: &jack::ProcessScope) -> Result<()> {
if !self.track_matrix.is_all_tracks_cleared() {
// Ignore tap if not all tracks are clear
return Ok(());
}
let current_frame_time = ps.last_frame_time();
let tempo_updated = self.tap_tempo.handle_tap(current_frame_time);
if let Some(frames_per_beat) = tempo_updated {
// If tempo was updated, we need to update all columns with new frames_per_beat
self.metronome.set_frames_per_beat(frames_per_beat);
self.persistence
.update_metronome_frames_per_beat(frames_per_beat)?;
self.track_matrix.set_frames_per_beat(frames_per_beat);
}
Ok(())
}
pub fn handle_button_6(&mut self) -> Result<()> { pub fn handle_button_6(&mut self) -> Result<()> {
self.selected_column = 0; self.selected_column = 0;
self.osc.selected_column_changed(self.selected_column)?; self.osc.selected_column_changed(self.selected_column)?;

View File

@ -0,0 +1,65 @@
/// Collects tap timestamps and calculates tempo when enough taps are received.
#[derive(Debug)]
pub struct TapTempo {
sample_rate: usize,
tap_times: [u32; Self::MAX_TAPS],
tap_count: usize,
}
impl TapTempo {
const MAX_TAPS: usize = 8;
const MIN_TAPS: usize = 4;
const MIN_INTERVAL_MS: usize = 100;
const MAX_INTERVAL_MS: usize = 2000;
pub fn new(sample_rate: usize) -> Self {
Self {
sample_rate,
tap_times: [0; Self::MAX_TAPS],
tap_count: 0,
}
}
pub fn handle_tap(&mut self, current_frame_time: u32) -> Option<usize> {
self.record_tap(current_frame_time);
self.calculate_tempo()
}
fn record_tap(&mut self, frame_time: u32) {
// Check if valid interval range
if self.tap_count > 0 {
let last_tap = self.tap_times[self.tap_count - 1];
if frame_time - last_tap < (Self::MIN_INTERVAL_MS * self.sample_rate / 1000) as u32 {
return;
}
if frame_time - last_tap > (Self::MAX_INTERVAL_MS * self.sample_rate / 1000) as u32 {
self.tap_count = 0;
}
}
// Add new entry
if self.tap_count < Self::MAX_TAPS {
self.tap_times[self.tap_count] = frame_time;
self.tap_count += 1;
} else {
self.tap_times[0] = frame_time;
self.tap_times.rotate_left(1);
}
}
fn calculate_tempo(&self) -> Option<usize> {
if self.tap_count < Self::MIN_TAPS {
return None;
}
let mut total_interval = 0u64;
let interval_count = self.tap_count - 1;
for i in 1..self.tap_count {
let interval = self.tap_times[i] - self.tap_times[i - 1];
total_interval += interval as u64;
}
let average_interval = (total_interval / interval_count as u64) as usize;
Some(average_interval)
}
}

View File

@ -107,6 +107,10 @@ impl Track {
self.audio_data.set_consolidated_buffer(buffer) self.audio_data.set_consolidated_buffer(buffer)
} }
pub fn is_empty(&self) -> bool {
matches!(self.current_state, TrackState::Empty) && self.audio_data.is_empty()
}
pub fn handle_xrun( pub fn handle_xrun(
&mut self, &mut self,
beat_in_missed: Option<u32>, beat_in_missed: Option<u32>,

View File

@ -40,6 +40,18 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> TrackMatrix<F, COLS,
self.columns[controllers.column()].handle_volume_update(new_volume, controllers) self.columns[controllers.column()].handle_volume_update(new_volume, controllers)
} }
pub fn is_all_tracks_cleared(&self) -> bool {
self.columns
.iter()
.all(|column| column.is_all_tracks_cleared())
}
pub fn set_frames_per_beat(&mut self, new_frames_per_beat: usize) {
for column in &mut self.columns {
column.set_frames_per_beat(new_frames_per_beat);
}
}
pub fn process( pub fn process(
&mut self, &mut self,
ps: &jack::ProcessScope, ps: &jack::ProcessScope,

View File

@ -43,7 +43,6 @@ impl SelectedRegister {
} }
} }
pub struct App { pub struct App {
pub structured_notes: Vec<StructuredNote>, pub structured_notes: Vec<StructuredNote>,
pub register_values: HashMap<String, u8>, pub register_values: HashMap<String, u8>,
@ -79,7 +78,8 @@ impl App {
cursor_position: 0, cursor_position: 0,
notes_list_state: ListState::default(), notes_list_state: ListState::default(),
command_sender, command_sender,
status_message: "Ready - Press 'n' for new note, ←→ for registers, 'h' for help".to_string(), status_message: "Ready - Press 'n' for new note, ←→ for registers, 'h' for help"
.to_string(),
show_help: false, show_help: false,
structured_form, structured_form,
} }
@ -91,9 +91,11 @@ impl App {
// Check if a note already exists for this register state // Check if a note already exists for this register state
let current_values = self.register_values.clone(); let current_values = self.register_values.clone();
if let Some(existing_note) = self.structured_notes.iter_mut().find(|n| { if let Some(existing_note) = self
n.register_values == current_values .structured_notes
}) { .iter_mut()
.find(|n| n.register_values == current_values)
{
*existing_note = note; *existing_note = note;
self.status_message = "Note updated".to_string(); self.status_message = "Note updated".to_string();
} else { } else {
@ -107,9 +109,11 @@ impl App {
// Check if a note already exists for this register state and pre-load it // Check if a note already exists for this register state and pre-load it
let current_values = self.register_values.clone(); let current_values = self.register_values.clone();
if let Some(existing_note) = self.structured_notes.iter().find(|n| { if let Some(existing_note) = self
n.register_values == current_values .structured_notes
}) { .iter()
.find(|n| n.register_values == current_values)
{
self.structured_form.current_note = existing_note.clone(); self.structured_form.current_note = existing_note.clone();
} }
@ -117,11 +121,15 @@ impl App {
} }
pub fn get_current_value(&self) -> u8 { pub fn get_current_value(&self) -> u8 {
*self.register_values.get(self.selected_register.as_str()).unwrap_or(&0) *self
.register_values
.get(self.selected_register.as_str())
.unwrap_or(&0)
} }
pub fn set_current_value(&mut self, value: u8) { pub fn set_current_value(&mut self, value: u8) {
self.register_values.insert(self.selected_register.as_str().to_string(), value); self.register_values
.insert(self.selected_register.as_str().to_string(), value);
self.send_midi_value(value); self.send_midi_value(value);
} }
@ -129,10 +137,9 @@ impl App {
let (cc_low, cc_high) = self.selected_register.cc_pair(); let (cc_low, cc_high) = self.selected_register.cc_pair();
if value <= 127 { if value <= 127 {
let _ = self.command_sender.send(MidiCommand::ControlChange { let _ = self
cc: cc_low, .command_sender
value, .send(MidiCommand::ControlChange { cc: cc_low, value });
});
} else { } else {
let _ = self.command_sender.send(MidiCommand::ControlChange { let _ = self.command_sender.send(MidiCommand::ControlChange {
cc: cc_high, cc: cc_high,
@ -146,15 +153,19 @@ impl App {
let mask = 1 << bit; let mask = 1 << bit;
let new_value = current ^ mask; let new_value = current ^ mask;
self.set_current_value(new_value); self.set_current_value(new_value);
self.status_message = format!("Toggled {}:bit{} ({})", self.status_message = format!(
self.selected_register.as_str(), bit, new_value); "Toggled {}:bit{} ({})",
self.selected_register.as_str(),
bit,
new_value
);
} }
pub fn save_mapping(&self, filename: &str) -> Result<(), Box<dyn Error>> { pub fn save_mapping(&self, filename: &str) -> Result<(), Box<dyn Error>> {
// Convert structured notes to legacy format for saving // Convert structured notes to legacy format for saving
let legacy_data = MappingData::from_structured_notes( let legacy_data = MappingData::from_structured_notes(
self.structured_notes.clone(), self.structured_notes.clone(),
self.register_values.clone() self.register_values.clone(),
); );
let json = serde_json::to_string_pretty(&legacy_data)?; let json = serde_json::to_string_pretty(&legacy_data)?;
fs::write(filename, json)?; fs::write(filename, json)?;

View File

@ -87,13 +87,13 @@ impl Default for MappingData {
impl StructuredNote { impl StructuredNote {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.switch_1.is_none() && self.switch_1.is_none()
self.switch_2.is_none() && && self.switch_2.is_none()
self.display_1.is_empty() && && self.display_1.is_empty()
self.display_2.is_empty() && && self.display_2.is_empty()
self.display_3.is_empty() && && self.display_3.is_empty()
self.button_leds.is_empty() && && self.button_leds.is_empty()
self.button_presses.is_empty() && self.button_presses.is_empty()
} }
pub fn generate_description(&self) -> String { pub fn generate_description(&self) -> String {
@ -101,11 +101,15 @@ impl StructuredNote {
if text.is_empty() { if text.is_empty() {
String::new() String::new()
} else { } else {
text.chars().map(|c| c.to_string()).collect::<Vec<_>>().join(", ") text.chars()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(", ")
} }
}; };
format!("{}{} {}{}{}{}{} {}{}{}{} {}{} [{}] [{}] [{}] [{}] [{}]", format!(
"{}{} {}{}{}{}{} {}{}{}{} {}{} [{}] [{}] [{}] [{}] [{}]",
self.switch_1.unwrap_or(0), self.switch_1.unwrap_or(0),
self.switch_2.unwrap_or(0), self.switch_2.unwrap_or(0),
self.switches.unwrap_or(0), self.switches.unwrap_or(0),
@ -244,7 +248,10 @@ impl StructuredNote {
} }
impl MappingData { impl MappingData {
pub fn from_structured_notes(notes: Vec<StructuredNote>, register_values: HashMap<String, u8>) -> Self { 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(); let legacy_notes = notes.iter().map(|n| n.to_register_note()).collect();
Self { Self {
notes: legacy_notes, notes: legacy_notes,

View File

@ -101,19 +101,32 @@ impl InputField {
} }
pub fn is_led_field(&self) -> bool { pub fn is_led_field(&self) -> bool {
matches!(self, matches!(
InputField::Switch1 | InputField::Switch2 | InputField::Switches | self,
InputField::Select | InputField::Number | InputField::Value1 | InputField::Switch1
InputField::Value2 | InputField::DirectSelect | InputField::MidiFunction | | InputField::Switch2
InputField::MidiChan | InputField::Config | InputField::ExpressionPedalA | | InputField::Switches
InputField::ExpressionPedalB | 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 { pub fn is_text_field(&self) -> bool {
matches!(self, matches!(
InputField::Display1 | InputField::Display2 | InputField::Display3 | self,
InputField::ButtonLeds | InputField::ButtonPresses InputField::Display1
| InputField::Display2
| InputField::Display3
| InputField::ButtonLeds
| InputField::ButtonPresses
) )
} }
} }
@ -217,15 +230,24 @@ pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, are
let is_current = form.current_field == *field; let is_current = form.current_field == *field;
let prefix = if is_current { "" } else { " " }; let prefix = if is_current { "" } else { " " };
let style = if is_current { let style = if is_current {
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else { } else {
Style::default() Style::default()
}; };
if field.is_led_field() { if field.is_led_field() {
let value = form.get_led_value(field); let value = form.get_led_value(field);
let value_str = match value { Some(1) => "1", Some(0) => "0", _ => "" }; let value_str = match value {
Line::from(vec![Span::styled(format!("{}{}: {}", prefix, field.name().to_lowercase(), value_str), style)]) Some(1) => "1",
Some(0) => "0",
_ => "",
};
Line::from(vec![Span::styled(
format!("{}{}: {}", prefix, field.name().to_lowercase(), value_str),
style,
)])
} else { } else {
let text = form.get_text_value(field); let text = form.get_text_value(field);
let display_text = if is_current { let display_text = if is_current {
@ -239,7 +261,15 @@ pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, are
} else { } else {
text.to_string() text.to_string()
}; };
Line::from(vec![Span::styled(format!("{}{} (list of active segments): {}", prefix, field.name().to_lowercase(), display_text), style)]) Line::from(vec![Span::styled(
format!(
"{}{} (list of active segments): {}",
prefix,
field.name().to_lowercase(),
display_text
),
style,
)])
} }
}; };
@ -278,7 +308,10 @@ pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, are
lines.push(create_field_line(&InputField::ButtonLeds, form)); lines.push(create_field_line(&InputField::ButtonLeds, form));
lines.push(create_field_line(&InputField::ButtonPresses, form)); lines.push(create_field_line(&InputField::ButtonPresses, form));
let block = Paragraph::new(lines) let block = Paragraph::new(lines).block(
.block(Block::default().borders(Borders::ALL).title("Structured Note Input")); Block::default()
.borders(Borders::ALL)
.title("Structured Note Input"),
);
f.render_widget(block, area); f.render_widget(block, area);
} }

View File

@ -1,27 +1,25 @@
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
event::{DisableMouseCapture, EnableMouseCapture},
};
use ratatui::{
backend::CrosstermBackend,
Terminal,
}; };
use kanal::{Receiver, Sender}; use kanal::{Receiver, Sender};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{error::Error, io}; use std::{error::Error, io};
mod midi; mod app;
mod data; mod data;
mod input_form; mod input_form;
mod midi;
mod ui; mod ui;
mod app;
use app::App; use app::App;
use midi::{MidiCommand, MidiSender}; use midi::{MidiCommand, MidiSender};
use ui::run_app; use ui::run_app;
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
// Create JACK client // Create JACK client
let (client, status) = jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?; let (client, status) =
jack::Client::new("FCB1010_Mapper", jack::ClientOptions::NO_START_SERVER)?;
if !status.is_empty() { if !status.is_empty() {
eprintln!("JACK client status: {:?}", status); eprintln!("JACK client status: {:?}", status);
} }

View File

@ -7,9 +7,7 @@ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{ widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
Block, Borders, Clear, List, ListItem, Paragraph, Wrap,
},
Frame, Terminal, Frame, Terminal,
}; };
use std::error::Error; use std::error::Error;
@ -23,7 +21,10 @@ pub fn ui(f: &mut Frame, app: &mut App) {
draw_structured_ui(f, app); draw_structured_ui(f, app);
// Input popup if needed // Input popup if needed
if matches!(app.input_mode, InputMode::EditValue | InputMode::SaveFile | InputMode::LoadFile) { if matches!(
app.input_mode,
InputMode::EditValue | InputMode::SaveFile | InputMode::LoadFile
) {
draw_input_popup(f, app); draw_input_popup(f, app);
} }
} }
@ -41,7 +42,11 @@ fn draw_structured_ui(f: &mut Frame, app: &mut App) {
// Title // Title
let title = Paragraph::new("FCB1010 Structured Note Mapper") let title = Paragraph::new("FCB1010 Structured Note Mapper")
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) .style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.alignment(Alignment::Center) .alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL)); .block(Block::default().borders(Borders::ALL));
f.render_widget(title, chunks[0]); f.render_widget(title, chunks[0]);
@ -81,7 +86,10 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) {
let ic03 = note.register_values.get("IC03").unwrap_or(&0); let ic03 = note.register_values.get("IC03").unwrap_or(&0);
let ic10 = note.register_values.get("IC10").unwrap_or(&0); let ic10 = note.register_values.get("IC10").unwrap_or(&0);
let ic11 = note.register_values.get("IC11").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)); content.push_str(&format!(
"[IC03:{:08b}, IC10:{:08b}, IC11:{:08b}]",
ic03, ic10, ic11
));
// Add dash separator and note description // Add dash separator and note description
content.push_str(&format!(" - {}. {}", i + 1, note.description)); content.push_str(&format!(" - {}. {}", i + 1, note.description));
@ -92,8 +100,16 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) {
let mut notes_list_state = app.notes_list_state.clone(); let mut notes_list_state = app.notes_list_state.clone();
let notes_list = List::new(items) let notes_list = List::new(items)
.block(Block::default().borders(Borders::ALL).title("Structured Notes (Enter to restore register values, Del to remove)")) .block(
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) 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(""); .highlight_symbol("");
f.render_stateful_widget(notes_list, area, &mut notes_list_state); f.render_stateful_widget(notes_list, area, &mut notes_list_state);
@ -102,7 +118,11 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) {
fn draw_register_values(f: &mut Frame, app: &App, area: Rect) { fn draw_register_values(f: &mut Frame, app: &App, area: Rect) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) .constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(34),
])
.split(area); .split(area);
let registers = [ let registers = [
@ -117,7 +137,10 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) {
let mut lines = Vec::new(); let mut lines = Vec::new();
lines.push(Line::from(vec![ lines.push(Line::from(vec![
Span::styled(format!("{}: ", name), Style::default().add_modifier(Modifier::BOLD)), Span::styled(
format!("{}: ", name),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(format!("{:08b} = {}", value, value)), Span::raw(format!("{:08b} = {}", value, value)),
])); ]));
@ -126,18 +149,26 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) {
for bit in (0..8).rev() { for bit in (0..8).rev() {
let is_set = (value >> bit) & 1 == 1; let is_set = (value >> bit) & 1 == 1;
let style = if is_set { let style = if is_set {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD)
} else { } else {
Style::default().fg(Color::DarkGray) Style::default().fg(Color::DarkGray)
}; };
bit_spans.push(Span::styled(format!("[{}]", if is_set { "" } else { "" }), style)); bit_spans.push(Span::styled(
format!("[{}]", if is_set { "" } else { "" }),
style,
));
} }
lines.push(Line::from(bit_spans)); lines.push(Line::from(bit_spans));
// Bit numbers // Bit numbers
let mut bit_num_spans = Vec::new(); let mut bit_num_spans = Vec::new();
for bit in (0..8).rev() { for bit in (0..8).rev() {
bit_num_spans.push(Span::styled(format!(" {} ", bit), Style::default().fg(Color::Yellow))); bit_num_spans.push(Span::styled(
format!(" {} ", bit),
Style::default().fg(Color::Yellow),
));
} }
lines.push(Line::from(bit_num_spans)); lines.push(Line::from(bit_num_spans));
@ -150,14 +181,17 @@ fn draw_register_values(f: &mut Frame, app: &App, area: Rect) {
let block = Block::default() let block = Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.style(border_style) .style(border_style)
.title(if is_selected { format!("{}", name) } else { name.to_string() }); .title(if is_selected {
format!("{}", name)
} else {
name.to_string()
});
let paragraph = Paragraph::new(lines).block(block); let paragraph = Paragraph::new(lines).block(block);
f.render_widget(paragraph, chunks[i]); f.render_widget(paragraph, chunks[i]);
} }
} }
fn draw_input_popup(f: &mut Frame, app: &App) { fn draw_input_popup(f: &mut Frame, app: &App) {
let area = centered_rect(60, 20, f.size()); let area = centered_rect(60, 20, f.size());
f.render_widget(Clear, area); f.render_widget(Clear, area);
@ -179,7 +213,12 @@ fn draw_input_popup(f: &mut Frame, app: &App) {
let chars: Vec<char> = app.input_buffer.chars().collect(); let chars: Vec<char> = app.input_buffer.chars().collect();
if chars.is_empty() { if chars.is_empty() {
spans.push(Span::styled("", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); spans.push(Span::styled(
"",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::REVERSED),
));
} else { } else {
if app.cursor_position > 0 { if app.cursor_position > 0 {
let before_cursor: String = chars.iter().take(app.cursor_position).collect(); let before_cursor: String = chars.iter().take(app.cursor_position).collect();
@ -188,19 +227,28 @@ fn draw_input_popup(f: &mut Frame, app: &App) {
if app.cursor_position < chars.len() { if app.cursor_position < chars.len() {
let cursor_char = chars[app.cursor_position]; let cursor_char = chars[app.cursor_position];
spans.push(Span::styled(cursor_char.to_string(), Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); spans.push(Span::styled(
cursor_char.to_string(),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::REVERSED),
));
if app.cursor_position + 1 < chars.len() { if app.cursor_position + 1 < chars.len() {
let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect(); let after_cursor: String = chars.iter().skip(app.cursor_position + 1).collect();
spans.push(Span::raw(after_cursor)); spans.push(Span::raw(after_cursor));
} }
} else { } else {
spans.push(Span::styled("", Style::default().fg(Color::White).add_modifier(Modifier::REVERSED))); spans.push(Span::styled(
"",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::REVERSED),
));
} }
} }
let paragraph = Paragraph::new(Line::from(spans)) let paragraph = Paragraph::new(Line::from(spans)).block(block);
.block(block);
f.render_widget(paragraph, area); f.render_widget(paragraph, area);
} }
@ -254,12 +302,11 @@ fn draw_help(f: &mut Frame) {
"Press 'h' again to return to main view", "Press 'h' again to return to main view",
]; ];
let help_paragraph = Paragraph::new( let help_paragraph = Paragraph::new(
help_text help_text
.iter() .iter()
.map(|&line| Line::from(line)) .map(|&line| Line::from(line))
.collect::<Vec<_>>() .collect::<Vec<_>>(),
) )
.block(Block::default().title("Help").borders(Borders::ALL)) .block(Block::default().title("Help").borders(Borders::ALL))
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -287,10 +334,7 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
.split(popup_layout[1])[1] .split(popup_layout[1])[1]
} }
pub fn run_app<B: Backend>( pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<(), Box<dyn Error>> {
terminal: &mut Terminal<B>,
mut app: App,
) -> Result<(), Box<dyn Error>> {
loop { loop {
terminal.draw(|f| ui(f, &mut app))?; terminal.draw(|f| ui(f, &mut app))?;
@ -312,7 +356,10 @@ pub fn handle_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<boo
} }
} }
fn handle_legacy_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<bool, Box<dyn Error>> { fn handle_legacy_input(
app: &mut App,
key: crossterm::event::KeyCode,
) -> Result<bool, Box<dyn Error>> {
match app.input_mode { match app.input_mode {
InputMode::Normal => handle_normal_input(app, key), InputMode::Normal => handle_normal_input(app, key),
InputMode::EditValue => handle_edit_value_input(app, key), InputMode::EditValue => handle_edit_value_input(app, key),
@ -322,7 +369,10 @@ fn handle_legacy_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
} }
} }
fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<bool, Box<dyn Error>> { fn handle_normal_input(
app: &mut App,
key: crossterm::event::KeyCode,
) -> Result<bool, Box<dyn Error>> {
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
match key { match key {
@ -439,7 +489,8 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
if app.structured_notes.is_empty() { if app.structured_notes.is_empty() {
app.notes_list_state.select(None); app.notes_list_state.select(None);
} else if selected >= app.structured_notes.len() { } else if selected >= app.structured_notes.len() {
app.notes_list_state.select(Some(app.structured_notes.len() - 1)); app.notes_list_state
.select(Some(app.structured_notes.len() - 1));
} }
app.status_message = "Note deleted".to_string(); app.status_message = "Note deleted".to_string();
} }
@ -452,7 +503,10 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
// Implement other input handlers (edit_value, save_file, load_file) // 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>> { fn handle_edit_value_input(
app: &mut App,
key: crossterm::event::KeyCode,
) -> Result<bool, Box<dyn Error>> {
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
match key { match key {
@ -460,8 +514,8 @@ fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Res
match app.input_buffer.parse::<u8>() { match app.input_buffer.parse::<u8>() {
Ok(value) => { Ok(value) => {
app.set_current_value(value); app.set_current_value(value);
app.status_message = format!("{} set to {}", app.status_message =
app.selected_register.as_str(), value); format!("{} set to {}", app.selected_register.as_str(), value);
} }
Err(_) => { Err(_) => {
app.status_message = "Invalid value (0-255)".to_string(); app.status_message = "Invalid value (0-255)".to_string();
@ -510,7 +564,9 @@ fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Res
} }
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
if let Ok(potential_value) = format!("{}{}", &app.input_buffer[..app.cursor_position], c).parse::<u16>() { if let Ok(potential_value) =
format!("{}{}", &app.input_buffer[..app.cursor_position], c).parse::<u16>()
{
if potential_value <= 255 { if potential_value <= 255 {
let mut chars: Vec<char> = app.input_buffer.chars().collect(); let mut chars: Vec<char> = app.input_buffer.chars().collect();
chars.insert(app.cursor_position, c); chars.insert(app.cursor_position, c);
@ -529,7 +585,10 @@ fn handle_edit_value_input(app: &mut App, key: crossterm::event::KeyCode) -> Res
Ok(false) Ok(false)
} }
fn handle_save_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<bool, Box<dyn Error>> { fn handle_save_file_input(
app: &mut App,
key: crossterm::event::KeyCode,
) -> Result<bool, Box<dyn Error>> {
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
match key { match key {
@ -591,7 +650,10 @@ fn handle_save_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Resu
Ok(false) Ok(false)
} }
fn handle_load_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<bool, Box<dyn Error>> { fn handle_load_file_input(
app: &mut App,
key: crossterm::event::KeyCode,
) -> Result<bool, Box<dyn Error>> {
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
match key { match key {

View File

@ -10,7 +10,10 @@ pub struct Args {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Command { pub enum Command {
Run, Run {
#[arg(short, long)]
release: bool,
},
Collect, Collect,
Mapper, Mapper,
} }

View File

@ -70,9 +70,12 @@ fn collect_directory_recursive(dir_path: &str, content: &mut String) {
let path = entry.path(); let path = entry.path();
if path.is_file() { if path.is_file() {
let file_path = path.to_string_lossy(); let file_path = path.to_string_lossy();
if file_path.ends_with(".bb") || file_path.ends_with(".bbappend") || if file_path.ends_with(".bb")
file_path.ends_with(".conf") || file_path.ends_with(".wks") || || file_path.ends_with(".bbappend")
file_path.ends_with(".service") { || file_path.ends_with(".conf")
|| file_path.ends_with(".wks")
|| file_path.ends_with(".service")
{
if let Ok(file_content) = fs::read_to_string(&path) { if let Ok(file_content) = fs::read_to_string(&path) {
content.push_str(&format!("\n===== FILE: {} =====\n", file_path)); content.push_str(&format!("\n===== FILE: {} =====\n", file_path));
content.push_str(&file_content); content.push_str(&file_content);

View File

@ -15,8 +15,11 @@ async fn main() {
.expect("Failed to change to workspace directory"); .expect("Failed to change to workspace directory");
match args.command { match args.command {
Some(args::Command::Run) | None => { Some(args::Command::Run { release }) => {
run::run().await; run::run(release).await;
}
None => {
run::run(false).await;
} }
Some(args::Command::Collect) => { Some(args::Command::Collect) => {
collect::collect().await; collect::collect().await;

View File

@ -108,7 +108,10 @@ async fn spawn_firmware_in_terminal() -> Child {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
Command::new("C:\\Program Files\\Git\\git-bash.exe") Command::new("C:\\Program Files\\Git\\git-bash.exe")
.args(["-c", "cd firmware && cargo run; read -p 'Press Enter to close...'"]) .args([
"-c",
"cd firmware && cargo run; read -p 'Press Enter to close...'",
])
.spawn() .spawn()
.expect("Could not start firmware debugger in git-bash") .expect("Could not start firmware debugger in git-bash")
} }
@ -143,9 +146,10 @@ async fn kill_process(child: &mut Child, name: &str) {
Err(e) => { Err(e) => {
// Don't print error if process already exited // Don't print error if process already exited
let error_msg = e.to_string().to_lowercase(); let error_msg = e.to_string().to_lowercase();
if !error_msg.contains("no such process") && if !error_msg.contains("no such process")
!error_msg.contains("not found") && && !error_msg.contains("not found")
!error_msg.contains("invalid argument") { && !error_msg.contains("invalid argument")
{
eprintln!("Failed to stop {}: {}", name, e); eprintln!("Failed to stop {}: {}", name, e);
} }
} }

View File

@ -5,7 +5,7 @@ use tokio_util::sync::CancellationToken;
type ProcessHandle = Arc<Mutex<Option<Child>>>; type ProcessHandle = Arc<Mutex<Option<Child>>>;
pub async fn run() { pub async fn run(release: bool) {
let qjackctl_handle: ProcessHandle = Arc::new(Mutex::new(None)); let qjackctl_handle: ProcessHandle = Arc::new(Mutex::new(None));
let audio_engine_handle: ProcessHandle = Arc::new(Mutex::new(None)); let audio_engine_handle: ProcessHandle = Arc::new(Mutex::new(None));
let gui_handle: ProcessHandle = Arc::new(Mutex::new(None)); let gui_handle: ProcessHandle = Arc::new(Mutex::new(None));
@ -35,9 +35,9 @@ pub async fn run() {
// Start processes // Start processes
let qjackctl = spawn_qjackctl().await; let qjackctl = spawn_qjackctl().await;
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let audio_engine = spawn_audio_engine().await; let audio_engine = spawn_audio_engine(release).await;
let gui = spawn_gui().await; let gui = spawn_gui(release).await;
let simulator = spawn_simulator().await; let simulator = spawn_simulator(release).await;
// Store handles for cleanup // Store handles for cleanup
*qjackctl_handle.lock().await = Some(qjackctl); *qjackctl_handle.lock().await = Some(qjackctl);
@ -130,11 +130,12 @@ async fn spawn_qjackctl() -> Child {
.expect("Could not start qjackctl") .expect("Could not start qjackctl")
} }
async fn spawn_audio_engine() -> Child { async fn spawn_audio_engine(release: bool) -> Child {
Command::new("cargo") Command::new("cargo")
.args([ .args([
"run", "run",
"--release", "--profile",
if release { "release" } else { "debug" },
"--bin", "--bin",
"audio_engine", "audio_engine",
"--", "--",
@ -145,19 +146,32 @@ async fn spawn_audio_engine() -> Child {
.expect("Could not start audio engine") .expect("Could not start audio engine")
} }
async fn spawn_gui() -> Child { async fn spawn_gui(release: bool) -> Child {
Command::new("cargo") Command::new("cargo")
.args(["run", "--release", "--bin", "gui"]) .args([
"run",
"--profile",
if release { "release" } else { "debug" },
"--bin",
"gui"
])
.spawn() .spawn()
.expect("Could not start gui") .expect("Could not start gui")
} }
async fn spawn_simulator() -> Child { async fn spawn_simulator(release: bool) -> Child {
if release {
Command::new("sleep")
.args(["infinity"])
.spawn()
.expect("Could not start simulator")
} else {
Command::new("cargo") Command::new("cargo")
.args(["run", "--release", "--bin", "simulator"]) .args(["run", "--release", "--bin", "simulator"])
.spawn() .spawn()
.expect("Could not start simulator") .expect("Could not start simulator")
} }
}
async fn kill_process(child: &mut Child, name: &str) { async fn kill_process(child: &mut Child, name: &str) {
match child.kill().await { match child.kill().await {