commit 483a422c8e124775b8da9584de2cbb0bf4bedd60
parent 3b772e3d1d8a9921257acd0d64170fa236196394
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Mon, 1 Jun 2026 15:21:33 -0500
UX3 complete: record-to-file TUI binding
Wires the (already-verified) recording backend into the TUI: 'R' on a selected
route toggles recording to ~/Music/Hydra/hydra-<route>-<ts>.wav; the route shows
a red ⏺ REC indicator while recording; status line reports the saved path +
duration on stop. Footer hint added. Backend (lock-free ring → WAV drain thread)
was verified earlier via afinfo (valid 2ch/44.1k/Float32 WAV with real signal).
26 tests green, 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 29 insertions(+), 0 deletions(-)
diff --git a/crates/hydra/src/app.rs b/crates/hydra/src/app.rs
@@ -374,6 +374,29 @@ impl App {
self.refresh();
}
+ /// Start or stop recording the selected route to a WAV file (daemon picks the path in
+ /// ~/Music/Hydra). Toggles based on the route's current recording state.
+ pub fn toggle_record_selected(&mut self) {
+ let Some(route) = self.routes.get(self.route_sel) else { return };
+ let id = route.id.clone();
+ let cmd = if route.recording {
+ Command::StopRecording { id }
+ } else {
+ Command::StartRecording { id, path: None }
+ };
+ match client::request(cmd) {
+ Ok(Response::RecordingStarted { path }) => self.status = format!("● recording → {path}"),
+ Ok(Response::RecordingStopped { path, frames }) => {
+ let secs = frames as f64 / 44100.0;
+ self.status = format!("saved {path} ({secs:.1}s)");
+ }
+ Ok(Response::Error(e)) => self.status = format!("record: {e}"),
+ Ok(other) => self.status = format!("unexpected: {other:?}"),
+ Err(e) => self.status = format!("record failed: {e}"),
+ }
+ self.refresh();
+ }
+
/// Scale the selected route's gain by `factor` (multiplicative, so a few presses span
/// the whole range). `up=true` multiplies, `up=false` divides. Clamped to 0..=16.
/// Multiplicative because Core Audio process taps attenuate ~-20 dB, so useful makeup
diff --git a/crates/hydra/src/main.rs b/crates/hydra/src/main.rs
@@ -105,6 +105,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
KeyCode::Char('p') => app.open_presets(),
KeyCode::Char('P') => app.begin_save_preset(),
KeyCode::Char('m') => app.toggle_mute_selected(),
+ KeyCode::Char('R') => app.toggle_record_selected(),
KeyCode::Char('d') | KeyCode::Char('x') => app.stop_selected(),
KeyCode::Char('+') | KeyCode::Char('=') => app.adjust_gain(true),
KeyCode::Char('-') | KeyCode::Char('_') => app.adjust_gain(false),
diff --git a/crates/hydra/src/ui.rs b/crates/hydra/src/ui.rs
@@ -195,6 +195,9 @@ fn draw_routes(f: &mut Frame, area: Rect, app: &App, theme: &Theme) {
Span::styled(format!("{:>3.0}% ", r.gain * 100.0), Style::default().fg(theme.accent)),
];
spans.extend(peak_bar(r.peak, 12, theme));
+ if r.recording {
+ spans.push(Span::styled(" ⏺ REC", Style::default().fg(theme.danger).add_modifier(Modifier::BOLD)));
+ }
ListItem::new(Line::from(spans))
})
.collect();
@@ -249,6 +252,8 @@ fn draw_footer(f: &mut Frame, area: Rect, app: &App, theme: &Theme) {
hint(" mute ", theme),
key("+/-", theme),
hint(" gain ", theme),
+ key("R", theme),
+ hint(" record ", theme),
key("d", theme),
hint(" stop ", theme),
key("⇥", theme),