Added tap tempo
This commit is contained in:
parent
0dd4387917
commit
872f933506
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -68,13 +68,22 @@ impl Metronome {
|
|||||||
Ok(timing)
|
Ok(timing)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_click_volume(&mut self, click_volume: f32, persistence: &mut PersistenceManagerController) -> Result<()> {
|
pub fn set_click_volume(
|
||||||
|
&mut self,
|
||||||
|
click_volume: f32,
|
||||||
|
persistence: &mut PersistenceManagerController,
|
||||||
|
) -> Result<()> {
|
||||||
let clamped = click_volume.clamp(0.0, 1.0);
|
let clamped = click_volume.clamp(0.0, 1.0);
|
||||||
persistence.update_metronome_volume(clamped)?;
|
persistence.update_metronome_volume(clamped)?;
|
||||||
self.click_volume = clamped;
|
self.click_volume = clamped;
|
||||||
Ok(())
|
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,
|
||||||
|
|||||||
@ -59,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()?;
|
||||||
|
|||||||
@ -16,7 +16,10 @@ pub enum PersistenceUpdate {
|
|||||||
},
|
},
|
||||||
UpdateMetronomeVolume {
|
UpdateMetronomeVolume {
|
||||||
volume: f32,
|
volume: f32,
|
||||||
}
|
},
|
||||||
|
UpdateMetronomeFramesPerBeat {
|
||||||
|
frames_per_beat: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersistenceManagerController {
|
impl PersistenceManagerController {
|
||||||
@ -34,7 +37,16 @@ impl PersistenceManagerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_metronome_volume(&self, volume: f32) -> Result<()> {
|
pub fn update_metronome_volume(&self, volume: f32) -> Result<()> {
|
||||||
let update = PersistenceUpdate::UpdateMetronomeVolume { volume: volume };
|
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) {
|
match self.sender.try_send(update) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
|
Ok(false) => Err(LooperError::StateSave(std::panic::Location::caller())), // Channel full
|
||||||
@ -120,6 +132,12 @@ impl PersistenceManager {
|
|||||||
PersistenceUpdate::UpdateMetronomeVolume { volume } => {
|
PersistenceUpdate::UpdateMetronomeVolume { volume } => {
|
||||||
self.state
|
self.state
|
||||||
.send_modify(|state| state.metronome.click_volume = volume);
|
.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()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +61,8 @@ impl<F: ChunkFactory, const COLS: usize, const ROWS: usize> ProcessHandler<F, CO
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_pedal_b(&mut self, value: u8) -> Result<()> {
|
pub fn handle_pedal_b(&mut self, value: u8) -> Result<()> {
|
||||||
self.metronome.set_click_volume(value as f32 / 127.0, &mut self.persistence)
|
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<()> {
|
||||||
@ -92,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,
|
||||||
@ -107,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)?;
|
||||||
|
|||||||
65
audio_engine/src/tap_tempo.rs
Normal file
65
audio_engine/src/tap_tempo.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>,
|
||||||
@ -67,9 +66,9 @@ impl App {
|
|||||||
map.insert("IC11".to_string(), 8);
|
map.insert("IC11".to_string(), 8);
|
||||||
map
|
map
|
||||||
};
|
};
|
||||||
|
|
||||||
let structured_form = StructuredInputForm::new(register_values.clone());
|
let structured_form = StructuredInputForm::new(register_values.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
structured_notes: Vec::new(),
|
structured_notes: Vec::new(),
|
||||||
register_values,
|
register_values,
|
||||||
@ -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,
|
||||||
}
|
}
|
||||||
@ -87,13 +87,15 @@ impl App {
|
|||||||
|
|
||||||
pub fn add_structured_note(&mut self) {
|
pub fn add_structured_note(&mut self) {
|
||||||
let note = self.structured_form.finalize_note();
|
let note = self.structured_form.finalize_note();
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -104,35 +106,40 @@ impl App {
|
|||||||
|
|
||||||
pub fn start_structured_input(&mut self) {
|
pub fn start_structured_input(&mut self) {
|
||||||
self.structured_form.reset(self.register_values.clone());
|
self.structured_form.reset(self.register_values.clone());
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.input_mode = InputMode::StructuredInput;
|
self.input_mode = InputMode::StructuredInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_midi_value(&self, value: u8) {
|
pub fn send_midi_value(&self, value: u8) {
|
||||||
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)?;
|
||||||
@ -164,11 +175,11 @@ impl App {
|
|||||||
pub fn load_mapping(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
|
pub fn load_mapping(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
|
||||||
let content = fs::read_to_string(filename)?;
|
let content = fs::read_to_string(filename)?;
|
||||||
let legacy_data: MappingData = serde_json::from_str(&content)?;
|
let legacy_data: MappingData = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
// Convert legacy format to structured notes for editing
|
// Convert legacy format to structured notes for editing
|
||||||
self.structured_notes = legacy_data.to_structured_notes();
|
self.structured_notes = legacy_data.to_structured_notes();
|
||||||
self.register_values = legacy_data.register_values.clone();
|
self.register_values = legacy_data.register_values.clone();
|
||||||
|
|
||||||
// Send current register values to hardware
|
// Send current register values to hardware
|
||||||
for (register, &value) in &legacy_data.register_values {
|
for (register, &value) in &legacy_data.register_values {
|
||||||
match register.as_str() {
|
match register.as_str() {
|
||||||
@ -193,7 +204,7 @@ impl App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +263,7 @@ impl App {
|
|||||||
fn handle_text_input(&mut self, key: KeyCode) {
|
fn handle_text_input(&mut self, key: KeyCode) {
|
||||||
let current_field = self.structured_form.current_field;
|
let current_field = self.structured_form.current_field;
|
||||||
let cursor_pos = self.structured_form.cursor_position;
|
let cursor_pos = self.structured_form.cursor_position;
|
||||||
|
|
||||||
if let Some(current_text) = self.structured_form.get_text_value_mut(¤t_field) {
|
if let Some(current_text) = self.structured_form.get_text_value_mut(¤t_field) {
|
||||||
match key {
|
match key {
|
||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
@ -271,7 +282,7 @@ impl App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cursor position after text operations
|
// Update cursor position after text operations
|
||||||
match key {
|
match key {
|
||||||
KeyCode::Char(_) => {
|
KeyCode::Char(_) => {
|
||||||
@ -303,4 +314,4 @@ impl App {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::collections::HashMap;
|
|||||||
pub struct StructuredNote {
|
pub struct StructuredNote {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub register_values: HashMap<String, u8>,
|
pub register_values: HashMap<String, u8>,
|
||||||
|
|
||||||
// LED states (0/1)
|
// LED states (0/1)
|
||||||
pub switch_1: Option<u8>,
|
pub switch_1: Option<u8>,
|
||||||
pub switch_2: Option<u8>,
|
pub switch_2: Option<u8>,
|
||||||
@ -20,12 +20,12 @@ pub struct StructuredNote {
|
|||||||
pub config: Option<u8>,
|
pub config: Option<u8>,
|
||||||
pub expression_pedal_a: Option<u8>,
|
pub expression_pedal_a: Option<u8>,
|
||||||
pub expression_pedal_b: Option<u8>,
|
pub expression_pedal_b: Option<u8>,
|
||||||
|
|
||||||
// Display segments (strings of active segments)
|
// Display segments (strings of active segments)
|
||||||
pub display_1: String,
|
pub display_1: String,
|
||||||
pub display_2: String,
|
pub display_2: String,
|
||||||
pub display_3: String,
|
pub display_3: String,
|
||||||
|
|
||||||
// Button states (strings of active button numbers)
|
// Button states (strings of active button numbers)
|
||||||
pub button_leds: String,
|
pub button_leds: String,
|
||||||
pub button_presses: String,
|
pub button_presses: String,
|
||||||
@ -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),
|
||||||
@ -146,7 +150,7 @@ impl StructuredNote {
|
|||||||
|
|
||||||
// Split the description into parts
|
// Split the description into parts
|
||||||
let parts: Vec<&str> = description.split_whitespace().collect();
|
let parts: Vec<&str> = description.split_whitespace().collect();
|
||||||
|
|
||||||
if parts.len() >= 4 {
|
if parts.len() >= 4 {
|
||||||
// Parse LED states from first 4 parts
|
// Parse LED states from first 4 parts
|
||||||
// Part 0: switch1+switch2 (e.g., "01")
|
// Part 0: switch1+switch2 (e.g., "01")
|
||||||
@ -160,7 +164,7 @@ impl StructuredNote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Part 1: switches+select+number+value1+value2 (e.g., "10000")
|
// Part 1: switches+select+number+value1+value2 (e.g., "10000")
|
||||||
if let Some(middle_bits) = parts.get(1) {
|
if let Some(middle_bits) = parts.get(1) {
|
||||||
let chars: Vec<char> = middle_bits.chars().collect();
|
let chars: Vec<char> = middle_bits.chars().collect();
|
||||||
@ -172,7 +176,7 @@ impl StructuredNote {
|
|||||||
structured_note.value_2 = chars[4].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")
|
// Part 2: direct_select+midi_function+midi_chan+config (e.g., "1101")
|
||||||
if let Some(last_bits) = parts.get(2) {
|
if let Some(last_bits) = parts.get(2) {
|
||||||
let chars: Vec<char> = last_bits.chars().collect();
|
let chars: Vec<char> = last_bits.chars().collect();
|
||||||
@ -183,7 +187,7 @@ impl StructuredNote {
|
|||||||
structured_note.config = chars[3].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")
|
// Part 3: expression_pedal_a+expression_pedal_b (e.g., "00")
|
||||||
if let Some(pedals) = parts.get(3) {
|
if let Some(pedals) = parts.get(3) {
|
||||||
let chars: Vec<char> = pedals.chars().collect();
|
let chars: Vec<char> = pedals.chars().collect();
|
||||||
@ -193,7 +197,7 @@ impl StructuredNote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the bracketed sections for text fields
|
// Parse the bracketed sections for text fields
|
||||||
let bracket_sections = Self::extract_bracket_sections(description);
|
let bracket_sections = Self::extract_bracket_sections(description);
|
||||||
if bracket_sections.len() >= 5 {
|
if bracket_sections.len() >= 5 {
|
||||||
@ -206,12 +210,12 @@ impl StructuredNote {
|
|||||||
|
|
||||||
Some(structured_note)
|
Some(structured_note)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_bracket_sections(text: &str) -> Vec<String> {
|
fn extract_bracket_sections(text: &str) -> Vec<String> {
|
||||||
let mut sections = Vec::new();
|
let mut sections = Vec::new();
|
||||||
let mut current_section = String::new();
|
let mut current_section = String::new();
|
||||||
let mut inside_brackets = false;
|
let mut inside_brackets = false;
|
||||||
|
|
||||||
for ch in text.chars() {
|
for ch in text.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
'[' => {
|
'[' => {
|
||||||
@ -238,13 +242,16 @@ impl StructuredNote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sections
|
sections
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@ -258,4 +265,4 @@ impl MappingData {
|
|||||||
.filter_map(StructuredNote::from_register_note)
|
.filter_map(StructuredNote::from_register_note)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +141,7 @@ impl StructuredInputForm {
|
|||||||
pub fn new(register_values: HashMap<String, u8>) -> Self {
|
pub fn new(register_values: HashMap<String, u8>) -> Self {
|
||||||
let mut current_note = StructuredNote::default();
|
let mut current_note = StructuredNote::default();
|
||||||
current_note.register_values = register_values;
|
current_note.register_values = register_values;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_note,
|
current_note,
|
||||||
current_field: InputField::Switch1,
|
current_field: InputField::Switch1,
|
||||||
@ -211,21 +224,30 @@ impl StructuredInputForm {
|
|||||||
|
|
||||||
pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, area: Rect) {
|
pub fn draw_structured_input_form(f: &mut Frame, form: &StructuredInputForm, area: Rect) {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
// Helper function to create field line
|
// Helper function to create field line
|
||||||
let create_field_line = |field: &InputField, form: &StructuredInputForm| -> Line {
|
let create_field_line = |field: &InputField, form: &StructuredInputForm| -> Line {
|
||||||
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()
|
||||||
} else {
|
.fg(Color::Yellow)
|
||||||
Style::default()
|
.add_modifier(Modifier::BOLD)
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
@ -61,4 +59,4 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,4 +50,4 @@ impl jack::ProcessHandler for MidiSender {
|
|||||||
|
|
||||||
jack::Control::Continue
|
jack::Control::Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
mapper/src/ui.rs
160
mapper/src/ui.rs
@ -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]);
|
||||||
@ -76,24 +81,35 @@ fn draw_structured_notes_list(f: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, note)| {
|
.map(|(i, note)| {
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
|
|
||||||
// Add register values in binary format first
|
// Add register values in binary format first
|
||||||
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));
|
||||||
|
|
||||||
ListItem::new(content)
|
ListItem::new(content)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
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);
|
||||||
@ -177,30 +211,44 @@ fn draw_input_popup(f: &mut Frame, app: &App) {
|
|||||||
// Create text with cursor using char-aware operations
|
// Create text with cursor using char-aware operations
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
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();
|
||||||
spans.push(Span::raw(before_cursor));
|
spans.push(Span::raw(before_cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +369,12 @@ 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 {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
let _ = app.command_sender.send(crate::midi::MidiCommand::Quit);
|
let _ = app.command_sender.send(crate::midi::MidiCommand::Quit);
|
||||||
@ -378,7 +428,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
|
|||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up => {
|
||||||
let notes_len = app.structured_notes.len();
|
let notes_len = app.structured_notes.len();
|
||||||
|
|
||||||
if let Some(selected) = app.notes_list_state.selected() {
|
if let Some(selected) = app.notes_list_state.selected() {
|
||||||
if selected > 0 {
|
if selected > 0 {
|
||||||
app.notes_list_state.select(Some(selected - 1));
|
app.notes_list_state.select(Some(selected - 1));
|
||||||
@ -389,7 +439,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
|
|||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down => {
|
||||||
let notes_len = app.structured_notes.len();
|
let notes_len = app.structured_notes.len();
|
||||||
|
|
||||||
if let Some(selected) = app.notes_list_state.selected() {
|
if let Some(selected) = app.notes_list_state.selected() {
|
||||||
if selected < notes_len.saturating_sub(1) {
|
if selected < notes_len.saturating_sub(1) {
|
||||||
app.notes_list_state.select(Some(selected + 1));
|
app.notes_list_state.select(Some(selected + 1));
|
||||||
@ -404,7 +454,7 @@ fn handle_normal_input(app: &mut App, key: crossterm::event::KeyCode) -> Result<
|
|||||||
// Restore register values from note
|
// Restore register values from note
|
||||||
app.register_values = note.register_values.clone();
|
app.register_values = note.register_values.clone();
|
||||||
app.status_message = "Register values restored from note".to_string();
|
app.status_message = "Register values restored from note".to_string();
|
||||||
|
|
||||||
// Send MIDI updates for all registers
|
// Send MIDI updates for all registers
|
||||||
for (register_name, &value) in &app.register_values {
|
for (register_name, &value) in &app.register_values {
|
||||||
match register_name.as_str() {
|
match register_name.as_str() {
|
||||||
@ -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,16 +503,19 @@ 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 {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
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,9 +585,12 @@ 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 {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
match app.save_mapping(&app.input_buffer) {
|
match app.save_mapping(&app.input_buffer) {
|
||||||
@ -591,9 +650,12 @@ 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 {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let filename = app.input_buffer.clone();
|
let filename = app.input_buffer.clone();
|
||||||
@ -648,4 +710,4 @@ fn handle_load_file_input(app: &mut App, key: crossterm::event::KeyCode) -> Resu
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,33 +27,33 @@ pub async fn collect_dir_output(dir: &str, output: &str) {
|
|||||||
|
|
||||||
pub async fn collect_image_output(output: &str) {
|
pub async fn collect_image_output(output: &str) {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
content.push_str("File Contents:\n");
|
content.push_str("File Contents:\n");
|
||||||
|
|
||||||
// Collect specific files mentioned by user
|
// Collect specific files mentioned by user
|
||||||
let files_to_collect = vec![
|
let files_to_collect = vec![
|
||||||
"image/conf/bblayers.conf",
|
"image/conf/bblayers.conf",
|
||||||
"image/conf/local.conf",
|
"image/conf/local.conf",
|
||||||
"image/conf/templateconf.cfg",
|
"image/conf/templateconf.cfg",
|
||||||
"image/docker-compose.yml",
|
"image/docker-compose.yml",
|
||||||
"image/Dockerfile",
|
"image/Dockerfile",
|
||||||
"image/implementation_plan.md",
|
"image/implementation_plan.md",
|
||||||
"image/run",
|
"image/run",
|
||||||
];
|
];
|
||||||
|
|
||||||
for file_path in files_to_collect {
|
for file_path in files_to_collect {
|
||||||
if let Ok(file_content) = fs::read_to_string(file_path) {
|
if let Ok(file_content) = fs::read_to_string(file_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect meta-fcb-looper files
|
// Collect meta-fcb-looper files
|
||||||
if fs::read_dir("image/meta-layers/meta-fcb-looper").is_ok() {
|
if fs::read_dir("image/meta-layers/meta-fcb-looper").is_ok() {
|
||||||
collect_directory_recursive("image/meta-layers/meta-fcb-looper", &mut content);
|
collect_directory_recursive("image/meta-layers/meta-fcb-looper", &mut content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to output file
|
// Write to output file
|
||||||
if let Err(e) = fs::write(output, content) {
|
if let Err(e) = fs::write(output, content) {
|
||||||
eprintln!("Failed to write output file: {}", e);
|
eprintln!("Failed to write output file: {}", e);
|
||||||
@ -64,15 +64,18 @@ pub async fn collect_image_output(output: &str) {
|
|||||||
|
|
||||||
fn collect_directory_recursive(dir_path: &str, content: &mut String) {
|
fn collect_directory_recursive(dir_path: &str, content: &mut String) {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
if let Ok(entries) = fs::read_dir(dir_path) {
|
if let Ok(entries) = fs::read_dir(dir_path) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
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);
|
||||||
|
|||||||
@ -26,10 +26,10 @@ pub async fn mapper() {
|
|||||||
println!("Starting qjackctl...");
|
println!("Starting qjackctl...");
|
||||||
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;
|
||||||
|
|
||||||
println!("Starting firmware debugger in new terminal...");
|
println!("Starting firmware debugger in new terminal...");
|
||||||
let firmware = spawn_firmware_in_terminal().await;
|
let firmware = spawn_firmware_in_terminal().await;
|
||||||
|
|
||||||
println!("Starting mapper...");
|
println!("Starting mapper...");
|
||||||
let mapper = spawn_mapper().await;
|
let mapper = spawn_mapper().await;
|
||||||
|
|
||||||
@ -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,11 +146,12 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user