use crossterm::event::{KeyCode, KeyEvent};
use crate::app::{App, FocusedBlock, Mode};
use crate::state::{EqBand, FilterType};
pub fn handle(key: KeyEvent, app: &mut App) {
match app.focused_block {
FocusedBlock::Devices => handle_devices(key, app),
FocusedBlock::Pipeline => handle_pipeline(key, app),
FocusedBlock::CommandBar => handle_command_bar(key, app),
}
}
fn handle_devices(key: KeyEvent, app: &mut App) {
match key.code {
KeyCode::Char('q') => app.quit(),
KeyCode::Tab | KeyCode::Char('l') => {
app.focused_block = FocusedBlock::Pipeline;
}
KeyCode::Down | KeyCode::Char('j') if !app.nodes.is_empty() => {
app.nodes_selected = (app.nodes_selected + 1) % app.nodes.len();
}
KeyCode::Up | KeyCode::Char('k') if !app.nodes.is_empty() => {
app.nodes_selected = app
.nodes_selected
.checked_sub(1)
.unwrap_or(app.nodes.len() - 1);
}
KeyCode::Char('c' | 'C') if !app.nodes.is_empty() => {
let target_node = &app.nodes[app.nodes_selected];
if target_node.description.contains("eqtui") {
return;
}
if let Err(e) = app.toggle_device_connection(target_node.id) {
tracing::error!(%e, "Failed to toggle device connection");
}
}
_ => {}
}
}
fn handle_pipeline(key: KeyEvent, app: &mut App) {
match key.code {
KeyCode::Char('q') => app.quit(),
KeyCode::Tab => {
app.focused_block = FocusedBlock::Devices;
}
KeyCode::Left | KeyCode::Char('h') if app.eq.column_selected > 1 => {
app.eq.column_selected -= 1;
}
KeyCode::Right | KeyCode::Char('l') if app.eq.column_selected < 4 => {
app.eq.column_selected += 1;
}
KeyCode::Down | KeyCode::Char('j') if !app.eq.bands.is_empty() => {
app.eq.band_selected = (app.eq.band_selected + 1) % app.eq.bands.len();
}
KeyCode::Up | KeyCode::Char('k') if !app.eq.bands.is_empty() => {
app.eq.band_selected = app
.eq
.band_selected
.checked_sub(1)
.unwrap_or(app.eq.bands.len() - 1);
}
KeyCode::Char('{') => app.switch_profile(-1),
KeyCode::Char('}') => app.switch_profile(1),
KeyCode::Char('a') => {
let freq = 1000.0;
let gain = 0.0;
let q = 1.0;
let ftype = FilterType::Peak;
let insert_at = (app.eq.band_selected + 1).min(app.eq.bands.len());
app.eq.bands.insert(
insert_at,
EqBand {
frequency: freq,
gain,
q,
filter_type: ftype,
},
);
app.eq.band_selected = insert_at;
}
KeyCode::Char('d') if app.last_key == Some('d') && !app.eq.bands.is_empty() => {
app.eq.bands.remove(app.eq.band_selected);
if app.eq.band_selected >= app.eq.bands.len() {
app.eq.band_selected = app.eq.bands.len().saturating_sub(1);
}
app.last_key = None;
return;
}
KeyCode::Char('i') => {
app.mode = Mode::Insert;
if !app.eq.bands.is_empty() {
let b = &app.eq.bands[app.eq.band_selected];
let val_str = match app.eq.column_selected {
1 => format!("{:.1}", b.frequency),
2 => format!("{:.1}", b.gain),
3 => format!("{:.2}", b.q),
4 => match b.filter_type {
FilterType::Peak => "PK".to_string(),
FilterType::LowShelf => "LS".to_string(),
FilterType::HighShelf => "HS".to_string(),
},
_ => String::new(),
};
app.eq.cell_input = tui_input::Input::new(val_str);
}
}
KeyCode::Char('v') => app.mode = Mode::Visual,
KeyCode::Char(':') => {
app.mode = Mode::Command;
app.command_input.reset();
}
KeyCode::Char('b') => {
app.eq.bypass = !app.eq.bypass;
let _ = app.sync_bypass();
}
KeyCode::Char('g') if app.last_key == Some('g') && !app.eq.bands.is_empty() => {
app.eq.band_selected = 0;
app.last_key = None;
return;
}
KeyCode::Char('r') if !app.eq.bands.is_empty() => {
let b = &mut app.eq.bands[app.eq.band_selected];
b.gain = 0.0;
b.q = 1.0;
}
KeyCode::Char('R') => {
for b in &mut app.eq.bands {
b.gain = 0.0;
b.q = 1.0;
}
}
KeyCode::Char('+' | '=') => {
if app.eq.bands.is_empty() {
return;
}
let b = &mut app.eq.bands[app.eq.band_selected];
match app.eq.column_selected {
1 => b.frequency = (b.frequency + 50.0).min(20000.0),
2 => b.gain += 0.5,
3 => b.q = (b.q + 0.1).min(10.0),
4 => {
b.filter_type = match b.filter_type {
FilterType::Peak => FilterType::LowShelf,
FilterType::LowShelf => FilterType::HighShelf,
FilterType::HighShelf => FilterType::Peak,
};
}
_ => {}
}
}
KeyCode::Char('-') => {
if app.eq.bands.is_empty() {
return;
}
let b = &mut app.eq.bands[app.eq.band_selected];
match app.eq.column_selected {
1 => b.frequency = (b.frequency - 50.0).max(20.0),
2 => b.gain -= 0.5,
3 => b.q = (b.q - 0.1).max(0.1),
4 => {
b.filter_type = match b.filter_type {
FilterType::Peak => FilterType::HighShelf,
FilterType::LowShelf => FilterType::Peak,
FilterType::HighShelf => FilterType::LowShelf,
};
}
_ => {}
}
}
_ => {}
}
app.last_key = match key.code {
KeyCode::Char(c) => Some(c),
_ => None,
};
}
fn handle_command_bar(_key: KeyEvent, app: &mut App) {
let _ = app;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crossterm::event::{KeyEvent, KeyModifiers};
use std::sync::Arc;
#[test]
fn test_handle_pipeline_horizontal_navigation() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Pipeline;
assert_eq!(app.eq.column_selected, 1);
handle_pipeline(
KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 2);
handle_pipeline(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), &mut app);
assert_eq!(app.eq.column_selected, 3);
handle_pipeline(
KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 4);
handle_pipeline(
KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 4);
handle_pipeline(
KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 3);
handle_pipeline(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), &mut app);
assert_eq!(app.eq.column_selected, 2);
handle_pipeline(
KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 1);
handle_pipeline(
KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.column_selected, 1);
}
#[test]
fn test_handle_pipeline_bumping() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Pipeline;
app.eq.bands.push(EqBand {
frequency: 1000.0,
gain: 0.0,
q: 1.0,
filter_type: FilterType::Peak,
});
app.eq.band_selected = 0;
app.eq.column_selected = 1;
handle_pipeline(
KeyEvent::new(KeyCode::Char('+'), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].frequency - 1050.0).abs() < f32::EPSILON);
handle_pipeline(
KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].frequency - 1000.0).abs() < f32::EPSILON);
app.eq.column_selected = 2;
handle_pipeline(
KeyEvent::new(KeyCode::Char('='), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].gain - 0.5).abs() < f32::EPSILON);
handle_pipeline(
KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].gain - 0.0).abs() < f32::EPSILON);
app.eq.column_selected = 3;
handle_pipeline(
KeyEvent::new(KeyCode::Char('+'), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].q - 1.1).abs() < f32::EPSILON);
handle_pipeline(
KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
&mut app,
);
assert!((app.eq.bands[0].q - 1.0).abs() < f32::EPSILON);
app.eq.column_selected = 4;
handle_pipeline(
KeyEvent::new(KeyCode::Char('+'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.bands[0].filter_type, FilterType::LowShelf);
handle_pipeline(
KeyEvent::new(KeyCode::Char('-'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.bands[0].filter_type, FilterType::Peak);
}
#[test]
fn test_handle_pipeline_insert_mode_initialization() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Pipeline;
app.eq.bands.push(EqBand {
frequency: 1000.0,
gain: 5.5,
q: 1.0,
filter_type: FilterType::Peak,
});
app.eq.band_selected = 0;
app.eq.column_selected = 1;
handle_pipeline(
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.mode, Mode::Insert);
assert_eq!(app.eq.cell_input.value(), "1000.0");
app.mode = Mode::Normal;
app.eq.column_selected = 2;
handle_pipeline(
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.cell_input.value(), "5.5");
app.mode = Mode::Normal;
app.eq.column_selected = 3;
handle_pipeline(
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.cell_input.value(), "1.00");
app.mode = Mode::Normal;
app.eq.column_selected = 4;
handle_pipeline(
KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
&mut app,
);
assert_eq!(app.eq.cell_input.value(), "PK");
}
#[test]
fn test_handle_devices_connect_toggle() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Devices;
app.filter_node_id = Some(42); app.nodes.push(crate::state::NodeInfo {
id: 123,
name: "Test Device".to_string(),
description: "Test Description".to_string(),
class: crate::state::DeviceClass::Speaker,
});
app.nodes_selected = 0;
handle_devices(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
&mut app,
);
assert!(app.is_device_connected(123));
assert_eq!(app.connected_devices, vec![123]);
handle_devices(
KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE),
&mut app,
);
assert!(!app.is_device_connected(123));
assert!(app.connected_devices.is_empty());
}
#[test]
fn test_handle_devices_connect_skips_eqtui() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Devices;
app.filter_node_id = Some(42);
app.nodes.push(crate::state::NodeInfo {
id: 99,
name: "eqtui Equalizer".to_string(),
description: "eqtui Equalizer".to_string(),
class: crate::state::DeviceClass::Speaker,
});
app.nodes_selected = 0;
handle_devices(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
&mut app,
);
assert!(app.connected_devices.is_empty());
}
#[test]
fn test_handle_devices_connect_no_filter_ready() {
let config = Arc::new(Config::default());
let mut app = App::new_test(config);
app.focused_block = FocusedBlock::Devices;
app.nodes.push(crate::state::NodeInfo {
id: 123,
name: "Test Device".to_string(),
description: "Test Description".to_string(),
class: crate::state::DeviceClass::Speaker,
});
app.nodes_selected = 0;
handle_devices(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
&mut app,
);
assert!(app.connected_devices.is_empty());
}
}