fix: replace ncurses with crossterm to fix CI builds on both arches

ncurses-rs is broken on Arch Linux ARM (missing TRUE constant in
generated bindings) and the git HEAD version breaks Ubuntu too.
crossterm is pure Rust with no C dependency, eliminating the issue
on all platforms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:01:36 +02:00
parent 0bb335e429
commit ccfb20f8bf
4 changed files with 360 additions and 100 deletions
+88 -68
View File
@@ -1,9 +1,17 @@
use clap::Parser;
use ncurses::*;
use ncurses::{LcCategory, setlocale};
use crossterm::{
cursor::{Hide, MoveTo},
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute, queue,
style::Print,
terminal::{
self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode,
},
};
use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
use std::collections::VecDeque;
use std::io::{stdin, Read};
use std::io::{stdout, Read, Write};
use std::fs::File;
use std::time::{Duration, Instant};
use byteorder::{LittleEndian, ReadBytesExt};
@@ -20,7 +28,7 @@ struct Cell {
}
struct ViewDiff {
changes: Vec<(i32, i32, char)>,
changes: Vec<(u16, u16, char)>,
}
impl ViewDiff {
@@ -33,13 +41,13 @@ impl ViewDiff {
}
fn add_change(&mut self, y: usize, x: usize, character: char) {
self.changes.push((y as i32, x as i32, character));
self.changes.push((y as u16, x as u16, character));
}
fn apply(&self) {
let mut buf = [0u8; 4];
let mut out = stdout();
for &(y, x, ch) in &self.changes {
let _ = mvprintw(y, x, ch.encode_utf8(&mut buf));
let _ = queue!(out, MoveTo(x, y), Print(ch));
}
}
}
@@ -56,14 +64,14 @@ fn render_bar(buf: &mut Vec<Vec<Cell>>, col: usize, sr: usize, bar_height_f: f32
}
fn handle_terminal_resize(
rows: &mut i32,
cols: &mut i32,
new_rows: i32,
new_cols: i32,
max_width: i32,
max_height: i32,
safe_rows: &mut i32,
safe_cols: &mut i32,
rows: &mut u16,
cols: &mut u16,
new_rows: u16,
new_cols: u16,
max_width: u16,
max_height: u16,
safe_rows: &mut u16,
safe_cols: &mut u16,
current_buffer: &mut Vec<Vec<Cell>>,
next_buffer: &mut Vec<Vec<Cell>>,
last_bars: &mut Vec<f32>,
@@ -95,8 +103,9 @@ fn handle_terminal_resize(
freq_mapping.push(index.min(fft_size / 2 - 1));
}
clear();
refresh();
let mut out = stdout();
let _ = queue!(out, Clear(ClearType::All));
let _ = out.flush();
}
#[derive(Parser)]
@@ -129,16 +138,18 @@ struct Cli {
fn main() {
let cli = Cli::parse();
let _ = setlocale(LcCategory::all, "");
initscr();
noecho();
curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
keypad(stdscr(), true);
timeout(0);
enable_raw_mode().expect("failed to enable raw mode");
let mut out = stdout();
execute!(out, EnterAlternateScreen, Hide).expect("failed to initialize terminal");
let mut rows: i32 = 0;
let mut cols: i32 = 0;
getmaxyx(stdscr(), &mut rows, &mut cols);
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let _ = disable_raw_mode();
let _ = execute!(stdout(), LeaveAlternateScreen);
default_hook(info);
}));
let (mut cols, mut rows) = terminal::size().expect("failed to get terminal size");
let sample_rate: usize = 48000;
let bit_depth: usize = 16;
@@ -161,26 +172,26 @@ fn main() {
PipeWire(std::process::ChildStdout),
}
refresh();
let _ = out.flush();
let mut audio_source = if cli.pipewire {
let process = Command::new("pw-record")
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start pw-record");
let stdout = process.stdout.expect("Failed to capture pw-record stdout");
refresh();
AudioSource::PipeWire(stdout)
.expect("failed to start pw-record");
let pipe = process.stdout.expect("failed to capture pw-record stdout");
let _ = out.flush();
AudioSource::PipeWire(pipe)
} else if let Some(filename) = &cli.input {
let file = File::open(filename).expect("Failed to open input file");
let file = File::open(filename).expect("failed to open input file");
AudioSource::File(file)
} else {
AudioSource::StdIn(stdin().lock())
AudioSource::StdIn(std::io::stdin().lock())
};
let max_width = 1000;
let max_height = 1000;
let max_width: u16 = 1000;
let max_height: u16 = 1000;
let mut safe_cols = cols.min(max_width);
let mut safe_rows = rows.min(max_height);
@@ -215,38 +226,44 @@ fn main() {
let mut max_magnitude: f32 = 1.0;
loop {
if last_terminal_resize.elapsed() >= resize_check_interval {
let mut new_rows = 0;
let mut new_cols = 0;
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
if new_rows != rows || new_cols != cols {
handle_terminal_resize(
&mut rows, &mut cols, new_rows, new_cols,
max_width, max_height, &mut safe_rows, &mut safe_cols,
&mut current_buffer, &mut next_buffer,
&mut last_bars, &mut freq_bands, &mut freq_mapping,
cli.log_power, fft_size,
);
sc = safe_cols as usize;
sr = safe_rows as usize;
if event::poll(Duration::ZERO).unwrap_or(false) {
match event::read() {
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
Ok(Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
})) => break,
Ok(Event::Resize(new_cols, new_rows)) => {
handle_terminal_resize(
&mut rows, &mut cols, new_rows, new_cols,
max_width, max_height, &mut safe_rows, &mut safe_cols,
&mut current_buffer, &mut next_buffer,
&mut last_bars, &mut freq_bands, &mut freq_mapping,
cli.log_power, fft_size,
);
sc = safe_cols as usize;
sr = safe_rows as usize;
}
_ => {}
}
last_terminal_resize = Instant::now();
}
let ch = getch();
if ch == KEY_RESIZE {
let mut new_rows = 0;
let mut new_cols = 0;
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
handle_terminal_resize(
&mut rows, &mut cols, new_rows, new_cols,
max_width, max_height, &mut safe_rows, &mut safe_cols,
&mut current_buffer, &mut next_buffer,
&mut last_bars, &mut freq_bands, &mut freq_mapping,
cli.log_power, fft_size,
);
sc = safe_cols as usize;
sr = safe_rows as usize;
if last_terminal_resize.elapsed() >= resize_check_interval {
if let Ok((new_cols, new_rows)) = terminal::size() {
if new_rows != rows || new_cols != cols {
handle_terminal_resize(
&mut rows, &mut cols, new_rows, new_cols,
max_width, max_height, &mut safe_rows, &mut safe_cols,
&mut current_buffer, &mut next_buffer,
&mut last_bars, &mut freq_bands, &mut freq_mapping,
cli.log_power, fft_size,
);
sc = safe_cols as usize;
sr = safe_rows as usize;
}
}
last_terminal_resize = Instant::now();
}
if last_frame_time.elapsed() >= interval {
@@ -261,7 +278,7 @@ fn main() {
let read_result = match &mut audio_source {
AudioSource::StdIn(input) => input.read(&mut input_buffer),
AudioSource::File(file) => file.read(&mut input_buffer),
AudioSource::PipeWire(stdout) => stdout.read(&mut input_buffer),
AudioSource::PipeWire(pipe) => pipe.read(&mut input_buffer),
};
match read_result {
@@ -313,7 +330,7 @@ fn main() {
}
view_diff.apply();
std::mem::swap(&mut current_buffer, &mut next_buffer);
refresh();
let _ = out.flush();
break;
}
}
@@ -379,7 +396,7 @@ fn main() {
view_diff.apply();
std::mem::swap(&mut current_buffer, &mut next_buffer);
last_frame_time = Instant::now();
refresh();
let _ = out.flush();
} else if buffer_is_empty {
for i in 0..sc {
last_bars[i] *= drop_factor;
@@ -397,8 +414,11 @@ fn main() {
view_diff.apply();
std::mem::swap(&mut current_buffer, &mut next_buffer);
last_frame_time = Instant::now();
refresh();
let _ = out.flush();
}
}
}
let _ = execute!(out, LeaveAlternateScreen);
let _ = disable_raw_mode();
}