Files
ferrosonic/src/app/cava.rs
Jamie Hewitt b94c12a301 Refactor app/mod.rs into focused submodules
Split the 2495-line mod.rs into 10 files by concern:
- playback.rs: playback controls and track management
- cava.rs: cava process management and VT100 parsing
- input.rs: event dispatch and global keybindings
- input_artists.rs: artists page keyboard handling
- input_queue.rs: queue page keyboard handling
- input_playlists.rs: playlists page keyboard handling
- input_server.rs: server page keyboard handling
- input_settings.rs: settings page keyboard handling
- mouse.rs: all mouse click and scroll handling
- mod.rs: App struct, new(), run(), event_loop(), load_initial_data()

Pure code reorganization — no behavioral changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:41:43 +00:00

244 lines
7.8 KiB
Rust

use std::io::Read as _;
use std::os::unix::io::FromRawFd;
use tracing::{error, info};
use super::*;
impl App {
/// Start cava process in noncurses mode via a pty
pub(super) fn start_cava(&mut self, cava_gradient: &[String; 8], cava_horizontal_gradient: &[String; 8], cava_size: u32) {
self.stop_cava();
// Compute pty dimensions to match the cava widget area
let (term_w, term_h) = crossterm::terminal::size().unwrap_or((80, 24));
let cava_h = (term_h as u32 * cava_size / 100).max(4) as u16;
let cava_w = term_w;
// Open a pty pair
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
unsafe {
if libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
) != 0
{
error!("openpty failed");
return;
}
// Set pty size so cava knows its dimensions
let ws = libc::winsize {
ws_row: cava_h,
ws_col: cava_w,
ws_xpixel: 0,
ws_ypixel: 0,
};
libc::ioctl(slave, libc::TIOCSWINSZ, &ws);
}
// Generate themed cava config and write to temp file
// Dup slave fd before converting to File (from_raw_fd takes ownership)
let slave_stdin_fd = unsafe { libc::dup(slave) };
let slave_stderr_fd = unsafe { libc::dup(slave) };
let slave_stdout = unsafe { std::fs::File::from_raw_fd(slave) };
let slave_stdin = unsafe { std::fs::File::from_raw_fd(slave_stdin_fd) };
let slave_stderr = unsafe { std::fs::File::from_raw_fd(slave_stderr_fd) };
let config_path = std::env::temp_dir().join("ferrosonic-cava.conf");
if let Err(e) = std::fs::write(&config_path, generate_cava_config(cava_gradient, cava_horizontal_gradient)) {
error!("Failed to write cava config: {}", e);
return;
}
let mut cmd = std::process::Command::new("cava");
cmd.arg("-p").arg(&config_path);
cmd.stdout(std::process::Stdio::from(slave_stdout))
.stderr(std::process::Stdio::from(slave_stderr))
.stdin(std::process::Stdio::from(slave_stdin))
.env("TERM", "xterm-256color");
match cmd.spawn() {
Ok(child) => {
// Set master to non-blocking
unsafe {
let flags = libc::fcntl(master, libc::F_GETFL);
libc::fcntl(master, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let master_file = unsafe { std::fs::File::from_raw_fd(master) };
let parser = vt100::Parser::new(cava_h, cava_w, 0);
self.cava_process = Some(child);
self.cava_pty_master = Some(master_file);
self.cava_parser = Some(parser);
info!("Cava started in noncurses mode ({}x{})", cava_w, cava_h);
}
Err(e) => {
error!("Failed to start cava: {}", e);
unsafe {
libc::close(master);
}
}
}
}
/// Stop cava process and clean up
pub(super) fn stop_cava(&mut self) {
if let Some(ref mut child) = self.cava_process {
let _ = child.kill();
let _ = child.wait();
}
self.cava_process = None;
self.cava_pty_master = None;
self.cava_parser = None;
}
/// Read cava pty output and snapshot screen to state
pub(super) async fn read_cava_output(&mut self) {
let (Some(ref mut master), Some(ref mut parser)) =
(&mut self.cava_pty_master, &mut self.cava_parser)
else {
return;
};
// Read all available bytes from the pty master
let mut buf = [0u8; 16384];
let mut got_data = false;
loop {
match master.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
parser.process(&buf[..n]);
got_data = true;
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(_) => return,
}
}
if !got_data {
return;
}
// Snapshot the vt100 screen into shared state
let screen = parser.screen();
let (rows, cols) = screen.size();
let mut cava_screen = Vec::with_capacity(rows as usize);
for row in 0..rows {
let mut spans: Vec<CavaSpan> = Vec::new();
let mut cur_text = String::new();
let mut cur_fg = CavaColor::Default;
let mut cur_bg = CavaColor::Default;
for col in 0..cols {
let cell = screen.cell(row, col).unwrap();
let fg = vt100_color_to_cava(cell.fgcolor());
let bg = vt100_color_to_cava(cell.bgcolor());
if fg != cur_fg || bg != cur_bg {
if !cur_text.is_empty() {
spans.push(CavaSpan {
text: std::mem::take(&mut cur_text),
fg: cur_fg,
bg: cur_bg,
});
}
cur_fg = fg;
cur_bg = bg;
}
let contents = cell.contents();
if contents.is_empty() {
cur_text.push(' ');
} else {
cur_text.push_str(&contents);
}
}
if !cur_text.is_empty() {
spans.push(CavaSpan {
text: cur_text,
fg: cur_fg,
bg: cur_bg,
});
}
cava_screen.push(CavaRow { spans });
}
let mut state = self.state.write().await;
state.cava_screen = cava_screen;
}
}
/// Convert vt100 color to our CavaColor type
fn vt100_color_to_cava(color: vt100::Color) -> CavaColor {
match color {
vt100::Color::Default => CavaColor::Default,
vt100::Color::Idx(i) => CavaColor::Indexed(i),
vt100::Color::Rgb(r, g, b) => CavaColor::Rgb(r, g, b),
}
}
/// Generate a cava configuration string with theme-appropriate gradient colors
pub(super) fn generate_cava_config(g: &[String; 8], h: &[String; 8]) -> String {
format!(
"\
[general]
framerate = 60
autosens = 1
overshoot = 0
bars = 0
bar_width = 1
bar_spacing = 0
lower_cutoff_freq = 10
higher_cutoff_freq = 18000
[input]
sample_rate = 96000
sample_bits = 32
remix = 1
[output]
method = noncurses
orientation = horizontal
channels = mono
mono_option = average
synchronized_sync = 1
disable_blanking = 1
[color]
gradient = 1
gradient_color_1 = '{g0}'
gradient_color_2 = '{g1}'
gradient_color_3 = '{g2}'
gradient_color_4 = '{g3}'
gradient_color_5 = '{g4}'
gradient_color_6 = '{g5}'
gradient_color_7 = '{g6}'
gradient_color_8 = '{g7}'
horizontal_gradient = 1
horizontal_gradient_color_1 = '{h0}'
horizontal_gradient_color_2 = '{h1}'
horizontal_gradient_color_3 = '{h2}'
horizontal_gradient_color_4 = '{h3}'
horizontal_gradient_color_5 = '{h4}'
horizontal_gradient_color_6 = '{h5}'
horizontal_gradient_color_7 = '{h6}'
horizontal_gradient_color_8 = '{h7}'
[smoothing]
monstercat = 0
waves = 0
noise_reduction = 11
",
g0 = g[0], g1 = g[1], g2 = g[2], g3 = g[3],
g4 = g[4], g5 = g[5], g6 = g[6], g7 = g[7],
h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3],
h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7],
)
}