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>
This commit is contained in:
243
src/app/cava.rs
Normal file
243
src/app/cava.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
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],
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user